package adauth_test

import (
	"context"
	"encoding/hex"
	"net"
	"strconv"
	"testing"

	"github.com/RedTeamPentesting/adauth"
)

const (
	testUser   = "someuser"
	testDomain = "domain.tld"
)

func TestUsernameAndDomainParsing(t *testing.T) {
	expectedUPN := testUser + "@" + testDomain
	expectedLogonName := testDomain + `\` + testUser
	expectedImpacketLogonName := testDomain + "/" + testUser

	testCases := []struct {
		Name string
		Opts adauth.Options
	}{
		{
			Name: "UPN",
			Opts: adauth.Options{User: testUser + "@" + testDomain},
		},
		{
			Name: "logon-name",
			Opts: adauth.Options{User: testDomain + `\` + testUser},
		},
		{
			Name: "impacket-style",
			Opts: adauth.Options{User: testDomain + `/` + testUser},
		},
		{
			Name: "pfx",
			Opts: adauth.Options{PFXFileName: "testdata/someuser@domain.tld.pfx"},
		},
	}

	for _, testCase := range testCases {
		testCase := testCase

		t.Run(testCase.Name, func(t *testing.T) {
			creds, err := testCase.Opts.NoTarget()
			if err != nil {
				t.Fatalf("get credentials: %v", err)
			}

			if creds.Username != testUser {
				t.Errorf("username %q is not %q", creds.Username, testUser)
			}

			if creds.Domain != testDomain {
				t.Errorf("domain %q is not %q", creds.Domain, testDomain)
			}

			upn := creds.UPN()
			if upn != expectedUPN {
				t.Errorf("UPN %q is not %q", upn, expectedUPN)
			}

			logonName := creds.LogonName()
			if logonName != expectedLogonName {
				t.Errorf("logon name %q is not %q", logonName, expectedLogonName)
			}

			impacketLogonName := creds.ImpacketLogonName()
			if impacketLogonName != expectedImpacketLogonName {
				t.Errorf("impacket-style logon name %q is not %q", impacketLogonName, expectedImpacketLogonName)
			}
		})
	}
}

func TestDomainFromTargetHostname(t *testing.T) {
	opts := adauth.Options{
		User: testUser,
	}

	creds, _, err := opts.WithTarget(context.Background(), "cifs", "host."+testDomain)
	if err != nil {
		t.Fatalf("get credentials: %v", err)
	}

	if creds.Domain != testDomain {
		t.Errorf("domain is %q instead of %q", creds.Domain, testDomain)
	}

	// ignore domain in hostname for local authentication
	opts = adauth.Options{
		User: "./" + testUser,
	}

	creds, _, err = opts.WithTarget(context.Background(), "cifs", "host."+testDomain)
	if err != nil {
		t.Fatalf("get credentials: %v", err)
	}

	if creds.Domain != "." {
		t.Errorf("domain is %q instead of %q", creds.Domain, ".")
	}
}

func TestDCTarget(t *testing.T) {
	dcHost := "dc." + testDomain
	expectedSPN := "ldap/" + dcHost

	opts := adauth.Options{
		User: testUser + `@` + testDomain,
		Resolver: &testResolver{
			SRV: map[string]map[string]map[string]struct {
				Name string
				SRV  []*net.SRV
			}{
				"ldap": {
					"tcp": {
						testDomain: {
							Name: dcHost,
							SRV: []*net.SRV{
								{Target: dcHost, Port: 389},
							},
						},
					},
				},
			},
		},
	}

	_, dc, err := opts.WithDCTarget(context.Background(), "ldap")
	if err != nil {
		t.Fatalf("get DC target: %v", err)
	}

	if dc.Address() != net.JoinHostPort(dcHost, "389") {
		t.Fatalf("DC address is %q instead of %q", dc.Address(), net.JoinHostPort(dcHost, "389"))
	}

	if dc.AddressWithoutPort() != dcHost {
		t.Fatalf("DC address without port is %q instead of %q", dc.Address(), dcHost)
	}

	if dc.Port != "389" {
		t.Errorf("DC port is %q instead of %q", dc.Port, "389")
	}

	spn, err := dc.SPN(context.Background())
	if err != nil {
		t.Fatalf("get DC SPN: %v", err)
	}

	if spn != expectedSPN {
		t.Errorf("DC SPN is %q instead of %q", spn, expectedSPN)
	}
}

func TestDCTargetWithoutSRVRecord(t *testing.T) {
	dcHost := "dc." + testDomain
	dcIP := net.ParseIP("10.0.0.1")
	expectedSPN := "ldap/" + dcHost

	opts := adauth.Options{
		User: testUser + `@` + testDomain,
		Resolver: &testResolver{
			HostToAddr: map[string][]net.IP{
				testDomain: {dcIP},
			},
			AddrToHost: map[string][]string{
				dcIP.String(): {dcHost},
			},
		},
	}

	_, dc, err := opts.WithDCTarget(context.Background(), "ldap")
	if err != nil {
		t.Fatalf("get DC target: %v", err)
	}

	if dc.Address() != net.JoinHostPort(dcHost, "389") {
		t.Fatalf("DC address is %q instead of %q", dc.Address(), net.JoinHostPort(dcHost, "389"))
	}

	if dc.AddressWithoutPort() != dcHost {
		t.Fatalf("DC address without port is %q instead of %q", dc.Address(), dcHost)
	}

	if dc.Port != "389" {
		t.Errorf("DC port is %q instead of %q", dc.Port, "389")
	}

	spn, err := dc.SPN(context.Background())
	if err != nil {
		t.Fatalf("get DC SPN: %v", err)
	}

	if spn != expectedSPN {
		t.Errorf("DC SPN is %q instead of %q", spn, expectedSPN)
	}
}

func TestDCTargetNoReverseLookup(t *testing.T) {
	dcIP := net.ParseIP("10.0.0.1")

	opts := adauth.Options{
		User: testUser + `@` + testDomain,
		Resolver: &testResolver{
			HostToAddr: map[string][]net.IP{
				testDomain: {dcIP},
			},
		},
	}

	_, dc, err := opts.WithDCTarget(context.Background(), "ldap")
	if err != nil {
		t.Fatalf("get DC target: %v", err)
	}

	if dc.Address() != net.JoinHostPort(dcIP.String(), "389") {
		t.Fatalf("DC address is %q instead of %q", dc.Address(), net.JoinHostPort(dcIP.String(), "389"))
	}
}

func TestKerberos(t *testing.T) {
	upn := testUser + `@` + testDomain

	testCases := []struct {
		Opts              adauth.Options
		ShouldUseKerberos bool
	}{
		{
			Opts: adauth.Options{
				User:     upn,
				Password: "pass",
			},
			ShouldUseKerberos: false,
		},
		{
			Opts: adauth.Options{
				User:   upn,
				AESKey: hex.EncodeToString(make([]byte, 16)),
			},
			ShouldUseKerberos: true,
		},
		{
			Opts: adauth.Options{
				User:     upn,
				Password: "test",
				AESKey:   hex.EncodeToString(make([]byte, 16)),
			},
			ShouldUseKerberos: false,
		},
		{
			Opts: adauth.Options{
				User:          upn,
				Password:      "pass",
				ForceKerberos: true,
			},
			ShouldUseKerberos: true,
		},
		{
			Opts: adauth.Options{
				User:        upn,
				PFXFileName: "testdata/someuser@domain.tld.pfx",
			},
			ShouldUseKerberos: false,
		},
		{
			Opts: adauth.Options{
				User: upn,
				// CCache indicates kerberos usage, but only if file is present
				// (even though it might be empty)
				CCache: "testdata/empty.ccache",
			},
			ShouldUseKerberos: true,
		},
	}

	for i, testCase := range testCases {
		testCase := testCase

		t.Run(strconv.Itoa(i), func(t *testing.T) {
			testCase.Opts.DomainController = "dc.domain.tld"

			_, target, err := testCase.Opts.WithDCTarget(context.Background(), "ldap")
			if err != nil {
				t.Fatalf("get target: %v", err)
			}

			switch {
			case testCase.ShouldUseKerberos && !target.UseKerberos:
				t.Errorf("target would not use Kerberos even though it should: %#v", testCase.Opts)
			case !testCase.ShouldUseKerberos && target.UseKerberos:
				t.Errorf("target would use Kerberos even though it should not: %#v", testCase.Opts)
			}
		})
	}
}

func TestCleanNTHash(t *testing.T) {
	ntHash := "31d6cfe0d16ae931b73c59d7e0c089c0"

	testCases := []string{
		ntHash,
		":" + ntHash,
		"aad3b435b51404eeaad3b435b51404ee:" + ntHash,
	}

	for i, testCase := range testCases {
		testCase := testCase

		t.Run(strconv.Itoa(i), func(t *testing.T) {
			creds, err := (&adauth.Options{
				User:   testUser + "@" + testDomain,
				NTHash: testCase,
			}).NoTarget()
			if err != nil {
				t.Fatalf("get credentials: %v", err)
			}

			if creds.NTHash != ntHash {
				t.Errorf("NT hash is %q instead of %q", creds.NTHash, ntHash)
			}
		})
	}
}
