//go:build integration

package integration

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"database/sql"
	"os"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/eggsampler/acme/v3"
	challtestsrvclient "github.com/letsencrypt/boulder/test/chall-test-srv-client"
	"github.com/letsencrypt/boulder/test/vars"
	"github.com/miekg/dns"
)

var expectedUserAgents = []string{"boulder", "remoteva-a", "remoteva-b", "remoteva-c"}

func collectUserAgentsFromDNSRequests(requests []challtestsrvclient.DNSRequest) []string {
	userAgents := make([]string, len(requests))
	for i, request := range requests {
		userAgents[i] = request.UserAgent
	}
	return userAgents
}

func assertUserAgentsLength(t *testing.T, got []string, checkType string) {
	t.Helper()

	if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" {
		// We only need 3 checks if the MPICFullResults feature-flag is not
		// enabled.
		//
		// TODO(#8121): Remove this once MPICFullResults has been defaulted to
		// true.
		if len(got) != 4 && len(got) != 3 {
			t.Errorf("During %s, expected 3 or 4 User-Agents, got %d", checkType, len(got))
		}
	} else {
		if len(got) != 4 {
			t.Errorf("During %s, expected 4 User-Agents, got %d", checkType, len(got))
		}
	}
}

func assertExpectedUserAgents(t *testing.T, got []string, checkType string) {
	t.Helper()

	if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" {
		// One User-Agent may be missing if the MPICFullResults feature-flag is
		// not enabled. This will need to be modified to 2 if we have not
		// removed this feature-flag by the time we get to 6+ perspectives.
		//
		// TODO(#8121): Remove this once MPICFullResults has been defaulted to
		// true.
		var alreadySkippedOne bool
		for _, ua := range expectedUserAgents {
			if !slices.Contains(got, ua) {
				if alreadySkippedOne {
					t.Errorf("During %s, missing more than 1 User-Agent in %s (got %v)", checkType, expectedUserAgents, got)
				}
				alreadySkippedOne = true
			}
		}
	} else {
		for _, ua := range expectedUserAgents {
			if !slices.Contains(got, ua) {
				t.Errorf("During %s, expected User-Agent %q in %s (got %v)", checkType, ua, expectedUserAgents, got)
			}
		}
	}
}

func TestMPICTLSALPN01(t *testing.T) {
	t.Parallel()

	client, err := makeClient()
	if err != nil {
		t.Fatalf("creating acme client: %s", err)
	}

	domain := randomDomain(t)

	order, err := client.Client.NewOrder(client.Account, []acme.Identifier{{Type: "dns", Value: domain}})
	if err != nil {
		t.Fatalf("creating order: %s", err)
	}

	authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0])
	if err != nil {
		t.Fatalf("fetching authorization: %s", err)
	}

	chal, ok := authz.ChallengeMap[acme.ChallengeTypeTLSALPN01]
	if !ok {
		t.Fatalf("no TLS-ALPN-01 challenge found in %#v", authz)
	}

	_, err = testSrvClient.AddARecord(domain, []string{"10.88.88.88"})
	if err != nil {
		t.Fatalf("adding A record: %s", err)
	}
	defer func() {
		testSrvClient.RemoveARecord(domain)
	}()

	_, err = testSrvClient.AddTLSALPN01Response(domain, chal.KeyAuthorization)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		_, err = testSrvClient.RemoveTLSALPN01Response(domain)
		if err != nil {
			t.Fatal(err)
		}
	}()

	chal, err = client.Client.UpdateChallenge(client.Account, chal)
	if err != nil {
		t.Fatalf("completing TLS-ALPN-01 validation: %s", err)
	}

	validationEvents, err := testSrvClient.TLSALPN01RequestHistory(domain)
	if err != nil {
		t.Fatal(err)
	}
	if os.Getenv("BOULDER_CONFIG_DIR") == "test/config-next" && len(validationEvents) != 4 {
		t.Errorf("expected 4 validation events got %d", len(validationEvents))
	} else if len(validationEvents) != 3 && len(validationEvents) != 4 {
		// We only need 3 checks if the MPICFullResults feature-flag is not
		// enabled.
		//
		// TODO(#8121): Remove this once MPICFullResults has been defaulted to
		// true.
		t.Errorf("expected 3 or 4 validation events got %d", len(validationEvents))
	}

	dnsEvents, err := testSrvClient.DNSRequestHistory(domain)
	if err != nil {
		t.Fatal(err)
	}

	var caaEvents []challtestsrvclient.DNSRequest
	for _, event := range dnsEvents {
		if event.Question.Qtype == dns.TypeCAA {
			caaEvents = append(caaEvents, event)
		}
	}
	assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
	if os.Getenv("BOULDER_CONFIG_DIR") == "test/config-next" {
		// We can only check the user-agent for DNS requests if the DOH
		// feature-flag is enabled.
		//
		// TODO(#8120): Remove this conditional once the DoH feature flag has
		// been defaulted to true.
		assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
	}
}

func TestMPICDNS01(t *testing.T) {
	t.Parallel()

	client, err := makeClient()
	if err != nil {
		t.Fatalf("creating acme client: %s", err)
	}

	domain := randomDomain(t)

	order, err := client.Client.NewOrder(client.Account, []acme.Identifier{{Type: "dns", Value: domain}})
	if err != nil {
		t.Fatalf("creating order: %s", err)
	}

	authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0])
	if err != nil {
		t.Fatalf("fetching authorization: %s", err)
	}

	chal, ok := authz.ChallengeMap[acme.ChallengeTypeDNS01]
	if !ok {
		t.Fatalf("no DNS challenge found in %#v", authz)
	}

	_, err = testSrvClient.AddDNS01Response(domain, chal.KeyAuthorization)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		_, err = testSrvClient.RemoveDNS01Response(domain)
		if err != nil {
			t.Fatal(err)
		}
	}()

	chal, err = client.Client.UpdateChallenge(client.Account, chal)
	if err != nil {
		t.Fatalf("completing DNS-01 validation: %s", err)
	}

	challDomainDNSEvents, err := testSrvClient.DNSRequestHistory("_acme-challenge." + domain)
	if err != nil {
		t.Fatal(err)
	}

	var validationEvents []challtestsrvclient.DNSRequest
	for _, event := range challDomainDNSEvents {
		if event.Question.Qtype == dns.TypeTXT && event.Question.Name == "_acme-challenge."+domain+"." {
			validationEvents = append(validationEvents, event)
		}
	}
	assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(validationEvents), "DNS-01 validation")
	if os.Getenv("BOULDER_CONFIG_DIR") == "test/config-next" {
		// We can only check the user-agent for DNS requests if the DOH
		// feature-flag is enabled.
		//
		// TODO(#8120): Remove this once the DoH feature flag has been defaulted
		// to true.
		assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(validationEvents), "DNS-01 validation")
	}

	domainDNSEvents, err := testSrvClient.DNSRequestHistory(domain)
	if err != nil {
		t.Fatal(err)
	}

	var caaEvents []challtestsrvclient.DNSRequest
	for _, event := range domainDNSEvents {
		if event.Question.Qtype == dns.TypeCAA {
			caaEvents = append(caaEvents, event)
		}
	}
	assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
	if os.Getenv("BOULDER_CONFIG_DIR") == "test/config-next" {
		// We can only check the user-agent for DNS requests if the DOH
		// feature-flag is enabled.
		//
		// TODO(#8120): Remove this once the DoH feature flag has been defaulted
		// to true.
		assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
	}
}

func TestMPICHTTP01(t *testing.T) {
	t.Parallel()

	client, err := makeClient()
	if err != nil {
		t.Fatalf("creating acme client: %s", err)
	}

	domain := randomDomain(t)

	order, err := client.Client.NewOrder(client.Account, []acme.Identifier{{Type: "dns", Value: domain}})
	if err != nil {
		t.Fatalf("creating order: %s", err)
	}

	authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0])
	if err != nil {
		t.Fatalf("fetching authorization: %s", err)
	}

	chal, ok := authz.ChallengeMap[acme.ChallengeTypeHTTP01]
	if !ok {
		t.Fatalf("no HTTP challenge found in %#v", authz)
	}

	_, err = testSrvClient.AddHTTP01Response(chal.Token, chal.KeyAuthorization)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		_, err = testSrvClient.RemoveHTTP01Response(chal.Token)
		if err != nil {
			t.Fatal(err)
		}
	}()

	chal, err = client.Client.UpdateChallenge(client.Account, chal)
	if err != nil {
		t.Fatalf("completing HTTP-01 validation: %s", err)
	}

	validationEvents, err := testSrvClient.HTTPRequestHistory(domain)
	if err != nil {
		t.Fatal(err)
	}

	var validationUAs []string
	for _, event := range validationEvents {
		if event.URL == "/.well-known/acme-challenge/"+chal.Token {
			validationUAs = append(validationUAs, event.UserAgent)
		}
	}
	assertUserAgentsLength(t, validationUAs, "HTTP-01 validation")
	assertExpectedUserAgents(t, validationUAs, "HTTP-01 validation")

	dnsEvents, err := testSrvClient.DNSRequestHistory(domain)
	if err != nil {
		t.Fatal(err)
	}

	var caaEvents []challtestsrvclient.DNSRequest
	for _, event := range dnsEvents {
		if event.Question.Qtype == dns.TypeCAA {
			caaEvents = append(caaEvents, event)
		}
	}

	assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
	if os.Getenv("BOULDER_CONFIG_DIR") == "test/config-next" {
		// We can only check the user-agent for DNS requests if the DOH
		// feature-flag is enabled.
		//
		// TODO(#8120): Remove this once the DoH feature flag has been defaulted
		// to true.
		assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
	}
}

func TestCAARechecking(t *testing.T) {
	t.Parallel()

	domain := randomDomain(t)
	idents := []acme.Identifier{{Type: "dns", Value: domain}}

	// Create an order and authorization, and fulfill the associated challenge.
	// This should put the authz into the "valid" state, since CAA checks passed.
	client, err := makeClient()
	if err != nil {
		t.Fatalf("creating acme client: %s", err)
	}

	order, err := client.Client.NewOrder(client.Account, idents)
	if err != nil {
		t.Fatalf("creating order: %s", err)
	}

	authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0])
	if err != nil {
		t.Fatalf("fetching authorization: %s", err)
	}

	chal, ok := authz.ChallengeMap[acme.ChallengeTypeHTTP01]
	if !ok {
		t.Fatalf("no HTTP challenge found in %#v", authz)
	}

	_, err = testSrvClient.AddHTTP01Response(chal.Token, chal.KeyAuthorization)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		_, err = testSrvClient.RemoveHTTP01Response(chal.Token)
		if err != nil {
			t.Fatal(err)
		}
	}()

	chal, err = client.Client.UpdateChallenge(client.Account, chal)
	if err != nil {
		t.Fatalf("completing HTTP-01 validation: %s", err)
	}

	// Manipulate the database so that it looks like the authz was validated
	// more than 8 hours ago.
	db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms)
	if err != nil {
		t.Fatalf("sql.Open: %s", err)
	}

	_, err = db.Exec(`UPDATE authz2 SET attemptedAt = ? WHERE identifierValue = ?`, time.Now().Add(-24*time.Hour).Format(time.DateTime), domain)
	if err != nil {
		t.Fatalf("updating authz attemptedAt timestamp: %s", err)
	}

	// Change the CAA record to now forbid issuance.
	_, err = testSrvClient.AddCAAIssue(domain, ";")
	if err != nil {
		t.Fatal(err)
	}

	// Try to finalize the order created above. Due to our db manipulation, this
	// should trigger a CAA recheck. And due to our challtestsrv manipulation,
	// that CAA recheck should fail. Therefore the whole finalize should fail.
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatalf("generating cert key: %s", err)
	}

	csr, err := makeCSR(key, idents, false)
	if err != nil {
		t.Fatalf("generating finalize csr: %s", err)
	}

	_, err = client.Client.FinalizeOrder(client.Account, order, csr)
	if err == nil {
		t.Errorf("expected finalize to fail, but got success")
	}
	if !strings.Contains(err.Error(), "CAA") {
		t.Errorf("expected finalize to fail due to CAA, but got: %s", err)
	}
}
