package identifier

import (
	"crypto/x509"
	"crypto/x509/pkix"
	"net"
	"net/netip"
	"reflect"
	"slices"
	"testing"

	corepb "github.com/letsencrypt/boulder/core/proto"
)

type withDefaultTestCases struct {
	Name        string
	InputIdents []*corepb.Identifier
	InputNames  []string
	want        ACMEIdentifiers
}

func (tc withDefaultTestCases) GetIdentifiers() []*corepb.Identifier {
	return tc.InputIdents
}

func (tc withDefaultTestCases) GetDnsNames() []string {
	return tc.InputNames
}

func TestFromProtoSliceWithDefault(t *testing.T) {
	testCases := []withDefaultTestCases{
		{
			Name: "Populated identifiers, populated names, same values",
			InputIdents: []*corepb.Identifier{
				{Type: "dns", Value: "a.example.com"},
				{Type: "dns", Value: "b.example.com"},
			},
			InputNames: []string{"a.example.com", "b.example.com"},
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "a.example.com"},
				{Type: TypeDNS, Value: "b.example.com"},
			},
		},
		{
			Name: "Populated identifiers, populated names, different values",
			InputIdents: []*corepb.Identifier{
				{Type: "dns", Value: "coffee.example.com"},
			},
			InputNames: []string{"tea.example.com"},
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "coffee.example.com"},
			},
		},
		{
			Name: "Populated identifiers, empty names",
			InputIdents: []*corepb.Identifier{
				{Type: "dns", Value: "example.com"},
			},
			InputNames: []string{},
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "example.com"},
			},
		},
		{
			Name: "Populated identifiers, nil names",
			InputIdents: []*corepb.Identifier{
				{Type: "dns", Value: "example.com"},
			},
			InputNames: nil,
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "example.com"},
			},
		},
		{
			Name:        "Empty identifiers, populated names",
			InputIdents: []*corepb.Identifier{},
			InputNames:  []string{"a.example.com", "b.example.com"},
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "a.example.com"},
				{Type: TypeDNS, Value: "b.example.com"},
			},
		},
		{
			Name:        "Empty identifiers, empty names",
			InputIdents: []*corepb.Identifier{},
			InputNames:  []string{},
			want:        nil,
		},
		{
			Name:        "Empty identifiers, nil names",
			InputIdents: []*corepb.Identifier{},
			InputNames:  nil,
			want:        nil,
		},
		{
			Name:        "Nil identifiers, populated names",
			InputIdents: nil,
			InputNames:  []string{"a.example.com", "b.example.com"},
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "a.example.com"},
				{Type: TypeDNS, Value: "b.example.com"},
			},
		},
		{
			Name:        "Nil identifiers, empty names",
			InputIdents: nil,
			InputNames:  []string{},
			want:        nil,
		},
		{
			Name:        "Nil identifiers, nil names",
			InputIdents: nil,
			InputNames:  nil,
			want:        nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.Name, func(t *testing.T) {
			t.Parallel()

			got := FromProtoSliceWithDefault(tc)
			if !slices.Equal(got, tc.want) {
				t.Errorf("Got %#v, but want %#v", got, tc.want)
			}
		})
	}
}

// TestFromX509 tests FromCert and FromCSR, which are fromX509's public
// wrappers.
func TestFromX509(t *testing.T) {
	cases := []struct {
		name        string
		subject     pkix.Name
		dnsNames    []string
		ipAddresses []net.IP
		want        ACMEIdentifiers
	}{
		{
			name:     "no explicit CN",
			dnsNames: []string{"a.com"},
			want:     ACMEIdentifiers{NewDNS("a.com")},
		},
		{
			name:     "explicit uppercase CN",
			subject:  pkix.Name{CommonName: "A.com"},
			dnsNames: []string{"a.com"},
			want:     ACMEIdentifiers{NewDNS("a.com")},
		},
		{
			name:     "no explicit CN, uppercase SAN",
			dnsNames: []string{"A.com"},
			want:     ACMEIdentifiers{NewDNS("a.com")},
		},
		{
			name:     "duplicate SANs",
			dnsNames: []string{"b.com", "b.com", "a.com", "a.com"},
			want:     ACMEIdentifiers{NewDNS("a.com"), NewDNS("b.com")},
		},
		{
			name:     "explicit CN not found in SANs",
			subject:  pkix.Name{CommonName: "a.com"},
			dnsNames: []string{"b.com"},
			want:     ACMEIdentifiers{NewDNS("a.com"), NewDNS("b.com")},
		},
		{
			name:        "mix of DNSNames and IPAddresses",
			dnsNames:    []string{"a.com"},
			ipAddresses: []net.IP{{192, 168, 1, 1}},
			want:        ACMEIdentifiers{NewDNS("a.com"), NewIP(netip.MustParseAddr("192.168.1.1"))},
		},
	}
	for _, tc := range cases {
		t.Run("cert/"+tc.name, func(t *testing.T) {
			t.Parallel()
			got := FromCert(&x509.Certificate{Subject: tc.subject, DNSNames: tc.dnsNames, IPAddresses: tc.ipAddresses})
			if !slices.Equal(got, tc.want) {
				t.Errorf("FromCert() got %#v, but want %#v", got, tc.want)
			}
		})
		t.Run("csr/"+tc.name, func(t *testing.T) {
			t.Parallel()
			got := FromCSR(&x509.CertificateRequest{Subject: tc.subject, DNSNames: tc.dnsNames, IPAddresses: tc.ipAddresses})
			if !slices.Equal(got, tc.want) {
				t.Errorf("FromCSR() got %#v, but want %#v", got, tc.want)
			}
		})
	}
}

func TestNormalize(t *testing.T) {
	cases := []struct {
		name   string
		idents ACMEIdentifiers
		want   ACMEIdentifiers
	}{
		{
			name: "convert to lowercase",
			idents: ACMEIdentifiers{
				{Type: TypeDNS, Value: "AlPha.example.coM"},
				{Type: TypeIP, Value: "fe80::CAFE"},
			},
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "alpha.example.com"},
				{Type: TypeIP, Value: "fe80::cafe"},
			},
		},
		{
			name: "sort",
			idents: ACMEIdentifiers{
				{Type: TypeDNS, Value: "foobar.com"},
				{Type: TypeDNS, Value: "bar.com"},
				{Type: TypeDNS, Value: "baz.com"},
				{Type: TypeDNS, Value: "a.com"},
				{Type: TypeIP, Value: "fe80::cafe"},
				{Type: TypeIP, Value: "2001:db8::1dea"},
				{Type: TypeIP, Value: "192.168.1.1"},
			},
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "a.com"},
				{Type: TypeDNS, Value: "bar.com"},
				{Type: TypeDNS, Value: "baz.com"},
				{Type: TypeDNS, Value: "foobar.com"},
				{Type: TypeIP, Value: "192.168.1.1"},
				{Type: TypeIP, Value: "2001:db8::1dea"},
				{Type: TypeIP, Value: "fe80::cafe"},
			},
		},
		{
			name: "de-duplicate",
			idents: ACMEIdentifiers{
				{Type: TypeDNS, Value: "AlPha.example.coM"},
				{Type: TypeIP, Value: "fe80::CAFE"},
				{Type: TypeDNS, Value: "alpha.example.com"},
				{Type: TypeIP, Value: "fe80::cafe"},
				NewIP(netip.MustParseAddr("fe80:0000:0000:0000:0000:0000:0000:cafe")),
			},
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "alpha.example.com"},
				{Type: TypeIP, Value: "fe80::cafe"},
			},
		},
		{
			name: "DNS before IP",
			idents: ACMEIdentifiers{
				{Type: TypeIP, Value: "fe80::cafe"},
				{Type: TypeDNS, Value: "alpha.example.com"},
			},
			want: ACMEIdentifiers{
				{Type: TypeDNS, Value: "alpha.example.com"},
				{Type: TypeIP, Value: "fe80::cafe"},
			},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := Normalize(tc.idents)
			if !slices.Equal(got, tc.want) {
				t.Errorf("Got %#v, but want %#v", got, tc.want)
			}
		})
	}
}

func TestToValues(t *testing.T) {
	cases := []struct {
		name            string
		idents          ACMEIdentifiers
		wantErr         string
		wantDnsNames    []string
		wantIpAddresses []net.IP
	}{
		{
			name: "DNS names and IP addresses",
			// These are deliberately out of alphabetical and type order, to
			// ensure ToValues doesn't do normalization, which ought to be done
			// explicitly.
			idents: ACMEIdentifiers{
				{Type: TypeDNS, Value: "beta.example.com"},
				{Type: TypeIP, Value: "fe80::cafe"},
				{Type: TypeDNS, Value: "alpha.example.com"},
				{Type: TypeIP, Value: "127.0.0.1"},
			},
			wantErr:         "",
			wantDnsNames:    []string{"beta.example.com", "alpha.example.com"},
			wantIpAddresses: []net.IP{net.ParseIP("fe80::cafe"), net.ParseIP("127.0.0.1")},
		},
		{
			name: "DNS names only",
			idents: ACMEIdentifiers{
				{Type: TypeDNS, Value: "alpha.example.com"},
				{Type: TypeDNS, Value: "beta.example.com"},
			},
			wantErr:         "",
			wantDnsNames:    []string{"alpha.example.com", "beta.example.com"},
			wantIpAddresses: nil,
		},
		{
			name: "IP addresses only",
			idents: ACMEIdentifiers{
				{Type: TypeIP, Value: "127.0.0.1"},
				{Type: TypeIP, Value: "fe80::cafe"},
			},
			wantErr:         "",
			wantDnsNames:    nil,
			wantIpAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("fe80::cafe")},
		},
		{
			name: "invalid IP address",
			idents: ACMEIdentifiers{
				{Type: TypeIP, Value: "fe80::c0ffee"},
			},
			wantErr:         "parsing IP address: fe80::c0ffee",
			wantDnsNames:    nil,
			wantIpAddresses: nil,
		},
		{
			name: "invalid identifier type",
			idents: ACMEIdentifiers{
				{Type: "fnord", Value: "panic.example.com"},
			},
			wantErr:         "evaluating identifier type: fnord for panic.example.com",
			wantDnsNames:    nil,
			wantIpAddresses: nil,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			gotDnsNames, gotIpAddresses, gotErr := tc.idents.ToValues()
			if !slices.Equal(gotDnsNames, tc.wantDnsNames) {
				t.Errorf("Got DNS names %#v, but want %#v", gotDnsNames, tc.wantDnsNames)
			}
			if !reflect.DeepEqual(gotIpAddresses, tc.wantIpAddresses) {
				t.Errorf("Got IP addresses %#v, but want %#v", gotIpAddresses, tc.wantIpAddresses)
			}
			if tc.wantErr != "" && (gotErr.Error() != tc.wantErr) {
				t.Errorf("Got error %#v, but want %#v", gotErr.Error(), tc.wantErr)
			}
			if tc.wantErr == "" && gotErr != nil {
				t.Errorf("Got error %#v, but didn't want one", gotErr.Error())
			}
		})
	}
}
