package va

import (
	"context"
	"fmt"
	"net"
	"strings"
	"testing"
	"time"

	"github.com/jmhodges/clock"
	"github.com/letsencrypt/boulder/bdns"
	"github.com/letsencrypt/boulder/core"
	"github.com/letsencrypt/boulder/identifier"
	"github.com/letsencrypt/boulder/metrics"
	"github.com/letsencrypt/boulder/probs"
	"github.com/letsencrypt/boulder/test"
	"github.com/prometheus/client_golang/prometheus"
)

func dnsChallenge() core.Challenge {
	return createChallenge(core.ChallengeTypeDNS01)
}

func TestDNSValidationEmpty(t *testing.T) {
	va, _ := setup(nil, 0, "", nil)

	// This test calls PerformValidation directly, because that is where the
	// metrics checked below are incremented.
	req := createValidationRequest("empty-txts.com", core.ChallengeTypeDNS01)
	res, _ := va.PerformValidation(context.Background(), req)
	test.AssertEquals(t, res.Problems.ProblemType, "unauthorized")
	test.AssertEquals(t, res.Problems.Detail, "No TXT record found at _acme-challenge.empty-txts.com")

	samples := test.CountHistogramSamples(va.metrics.validationTime.With(prometheus.Labels{
		"type":         "dns-01",
		"result":       "invalid",
		"problem_type": "unauthorized",
	}))
	if samples != 1 {
		t.Errorf("Wrong number of samples for invalid validation. Expected 1, got %d", samples)
	}
}

func TestDNSValidationWrong(t *testing.T) {
	va, _ := setup(nil, 0, "", nil)

	_, prob := va.validateDNS01(context.Background(), dnsi("wrong-dns01.com"), dnsChallenge())
	if prob == nil {
		t.Fatalf("Successful DNS validation with wrong TXT record")
	}
	test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"a\" found at _acme-challenge.wrong-dns01.com")
}

func TestDNSValidationWrongMany(t *testing.T) {
	va, _ := setup(nil, 0, "", nil)

	_, prob := va.validateDNS01(context.Background(), dnsi("wrong-many-dns01.com"), dnsChallenge())
	if prob == nil {
		t.Fatalf("Successful DNS validation with wrong TXT record")
	}
	test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"a\" (and 4 more) found at _acme-challenge.wrong-many-dns01.com")
}

func TestDNSValidationWrongLong(t *testing.T) {
	va, _ := setup(nil, 0, "", nil)

	_, prob := va.validateDNS01(context.Background(), dnsi("long-dns01.com"), dnsChallenge())
	if prob == nil {
		t.Fatalf("Successful DNS validation with wrong TXT record")
	}
	test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\" found at _acme-challenge.long-dns01.com")
}

func TestDNSValidationFailure(t *testing.T) {
	va, _ := setup(nil, 0, "", nil)

	_, prob := va.validateDNS01(ctx, dnsi("localhost"), dnsChallenge())

	test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
}

func TestDNSValidationInvalid(t *testing.T) {
	var notDNS = identifier.ACMEIdentifier{
		Type:  identifier.IdentifierType("iris"),
		Value: "790DB180-A274-47A4-855F-31C428CB1072",
	}

	va, _ := setup(nil, 0, "", nil)

	_, prob := va.validateDNS01(ctx, notDNS, dnsChallenge())

	test.AssertEquals(t, prob.Type, probs.MalformedProblem)
}

func TestDNSValidationNotSane(t *testing.T) {
	va, _ := setup(nil, 0, "", nil)

	chall := dnsChallenge()
	chall.Token = ""
	_, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
	if prob.Type != probs.MalformedProblem {
		t.Errorf("Got wrong error type: expected %s, got %s",
			prob.Type, probs.MalformedProblem)
	}
	if !strings.Contains(prob.Error(), "Challenge failed consistency check:") {
		t.Errorf("Got wrong error: %s", prob.Error())
	}

	chall.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_"
	_, prob = va.validateChallenge(ctx, dnsi("localhost"), chall)
	if prob.Type != probs.MalformedProblem {
		t.Errorf("Got wrong error type: expected %s, got %s",
			prob.Type, probs.MalformedProblem)
	}
	if !strings.Contains(prob.Error(), "Challenge failed consistency check:") {
		t.Errorf("Got wrong error: %s", prob.Error())
	}

	chall.ProvidedKeyAuthorization = "a"
	_, prob = va.validateChallenge(ctx, dnsi("localhost"), chall)
	if prob.Type != probs.MalformedProblem {
		t.Errorf("Got wrong error type: expected %s, got %s",
			prob.Type, probs.MalformedProblem)
	}
	if !strings.Contains(prob.Error(), "Challenge failed consistency check:") {
		t.Errorf("Got wrong error: %s", prob.Error())
	}

}

func TestDNSValidationServFail(t *testing.T) {
	va, _ := setup(nil, 0, "", nil)

	_, prob := va.validateChallenge(ctx, dnsi("servfail.com"), dnsChallenge())

	test.AssertEquals(t, prob.Type, probs.DNSProblem)
}

func TestDNSValidationNoServer(t *testing.T) {
	va, log := setup(nil, 0, "", nil)
	va.dnsClient = bdns.NewTest(
		time.Second*5,
		nil,
		metrics.NoopRegisterer,
		clock.New(),
		1,
		log)

	_, prob := va.validateChallenge(ctx, dnsi("localhost"), dnsChallenge())

	test.AssertEquals(t, prob.Type, probs.DNSProblem)
}

func TestDNSValidationOK(t *testing.T) {
	va, _ := setup(nil, 0, "", nil)

	_, prob := va.validateChallenge(ctx, dnsi("good-dns01.com"), dnsChallenge())

	test.Assert(t, prob == nil, "Should be valid.")
}

func TestDNSValidationNoAuthorityOK(t *testing.T) {
	va, _ := setup(nil, 0, "", nil)

	_, prob := va.validateChallenge(ctx, dnsi("no-authority-dns01.com"), dnsChallenge())

	test.Assert(t, prob == nil, "Should be valid.")
}

func TestAvailableAddresses(t *testing.T) {
	v6a := net.ParseIP("::1")
	v6b := net.ParseIP("2001:db8::2:1") // 2001:DB8 is reserved for docs (RFC 3849)
	v4a := net.ParseIP("127.0.0.1")
	v4b := net.ParseIP("192.0.2.1") // 192.0.2.0/24 is reserved for docs (RFC 5737)

	testcases := []struct {
		input []net.IP
		v4    []net.IP
		v6    []net.IP
	}{
		// An empty validation record
		{
			[]net.IP{},
			[]net.IP{},
			[]net.IP{},
		},
		// A validation record with one IPv4 address
		{
			[]net.IP{v4a},
			[]net.IP{v4a},
			[]net.IP{},
		},
		// A dual homed record with an IPv4 and IPv6 address
		{
			[]net.IP{v4a, v6a},
			[]net.IP{v4a},
			[]net.IP{v6a},
		},
		// The same as above but with the v4/v6 order flipped
		{
			[]net.IP{v6a, v4a},
			[]net.IP{v4a},
			[]net.IP{v6a},
		},
		// A validation record with just IPv6 addresses
		{
			[]net.IP{v6a, v6b},
			[]net.IP{},
			[]net.IP{v6a, v6b},
		},
		// A validation record with interleaved IPv4/IPv6 records
		{
			[]net.IP{v6a, v4a, v6b, v4b},
			[]net.IP{v4a, v4b},
			[]net.IP{v6a, v6b},
		},
	}

	for _, tc := range testcases {
		// Split the input record into v4/v6 addresses
		v4result, v6result := availableAddresses(tc.input)

		// Test that we got the right number of v4 results
		test.Assert(t, len(tc.v4) == len(v4result),
			fmt.Sprintf("Wrong # of IPv4 results: expected %d, got %d", len(tc.v4), len(v4result)))

		// Check that all of the v4 results match expected values
		for i, v4addr := range tc.v4 {
			test.Assert(t, v4addr.String() == v4result[i].String(),
				fmt.Sprintf("Wrong v4 result index %d: expected %q got %q", i, v4addr.String(), v4result[i].String()))
		}

		// Test that we got the right number of v6 results
		test.Assert(t, len(tc.v6) == len(v6result),
			fmt.Sprintf("Wrong # of IPv6 results: expected %d, got %d", len(tc.v6), len(v6result)))

		// Check that all of the v6 results match expected values
		for i, v6addr := range tc.v6 {
			test.Assert(t, v6addr.String() == v6result[i].String(),
				fmt.Sprintf("Wrong v6 result index %d: expected %q got %q", i, v6addr.String(), v6result[i].String()))
		}
	}
}
