package ctrld

import (
	"context"
	"net/url"
	"sync"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) {
	tests := []struct {
		name string
		uc   *UpstreamConfig
	}{
		{
			name: "doh/doh3",
			uc: &UpstreamConfig{
				Name:     "doh",
				Type:     ResolverTypeDOH,
				Endpoint: "https://freedns.controld.com/p2",
				Timeout:  5000,
			},
		},
		{
			name: "doq/dot",
			uc: &UpstreamConfig{
				Name:     "dot",
				Type:     ResolverTypeDOT,
				Endpoint: "p2.freedns.controld.com",
				Timeout:  5000,
			},
		},
	}
	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			// Enable parallel tests once https://github.com/microsoft/wmi/issues/165 fixed.
			// t.Parallel()
			tc.uc.Init(context.Background())
			tc.uc.SetupBootstrapIP(context.Background())
			if len(tc.uc.bootstrapIPs) == 0 {
				t.Log(defaultNameservers(context.Background()))
				t.Fatalf("could not bootstrap ip: %s", tc.uc.String())
			}
		})
	}

}

func TestUpstreamConfig_Init(t *testing.T) {
	u1, _ := url.Parse("https://example.com")
	u2, _ := url.Parse("https://example.com?k=v")
	u3, _ := url.Parse("https://freedns.controld.com/p1")
	tests := []struct {
		name     string
		uc       *UpstreamConfig
		expected *UpstreamConfig
	}{
		{
			"doh+doh3",
			&UpstreamConfig{
				Name:        "doh",
				Type:        "doh",
				Endpoint:    "https://example.com",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
			},
			&UpstreamConfig{
				Name:        "doh",
				Type:        "doh",
				Endpoint:    "https://example.com",
				BootstrapIP: "",
				Domain:      "example.com",
				Timeout:     0,
				IPStack:     IpStackBoth,
				u:           u1,
			},
		},
		{
			"doh+doh3 with query param",
			&UpstreamConfig{
				Name:        "doh",
				Type:        "doh",
				Endpoint:    "https://example.com?k=v",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
			},
			&UpstreamConfig{
				Name:        "doh",
				Type:        "doh",
				Endpoint:    "https://example.com?k=v",
				BootstrapIP: "",
				Domain:      "example.com",
				Timeout:     0,
				IPStack:     IpStackBoth,
				u:           u2,
			},
		},
		{
			"dot+doq",
			&UpstreamConfig{
				Name:        "dot",
				Type:        "dot",
				Endpoint:    "freedns.controld.com:8853",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
			},
			&UpstreamConfig{
				Name:        "dot",
				Type:        "dot",
				Endpoint:    "freedns.controld.com:8853",
				BootstrapIP: "",
				Domain:      "freedns.controld.com",
				Timeout:     0,
				IPStack:     IpStackSplit,
			},
		},
		{
			"dot+doq without port",
			&UpstreamConfig{
				Name:        "dot",
				Type:        "dot",
				Endpoint:    "freedns.controld.com",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
				IPStack:     IpStackSplit,
			},
			&UpstreamConfig{
				Name:        "dot",
				Type:        "dot",
				Endpoint:    "freedns.controld.com:853",
				BootstrapIP: "",
				Domain:      "freedns.controld.com",
				Timeout:     0,
				IPStack:     IpStackSplit,
			},
		},
		{
			"legacy",
			&UpstreamConfig{
				Name:        "legacy",
				Type:        "legacy",
				Endpoint:    "1.2.3.4:53",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
			},
			&UpstreamConfig{
				Name:        "legacy",
				Type:        "legacy",
				Endpoint:    "1.2.3.4:53",
				BootstrapIP: "1.2.3.4",
				Domain:      "1.2.3.4",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
		},
		{
			"legacy without port",
			&UpstreamConfig{
				Name:        "legacy",
				Type:        "legacy",
				Endpoint:    "1.2.3.4",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
			},
			&UpstreamConfig{
				Name:        "legacy",
				Type:        "legacy",
				Endpoint:    "1.2.3.4:53",
				BootstrapIP: "1.2.3.4",
				Domain:      "1.2.3.4",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
		},
		{
			"doh+doh3 with send client info set",
			&UpstreamConfig{
				Name:           "doh",
				Type:           "doh",
				Endpoint:       "https://example.com?k=v",
				BootstrapIP:    "",
				Domain:         "",
				Timeout:        0,
				SendClientInfo: ptrBool(false),
				IPStack:        IpStackBoth,
			},
			&UpstreamConfig{
				Name:           "doh",
				Type:           "doh",
				Endpoint:       "https://example.com?k=v",
				BootstrapIP:    "",
				Domain:         "example.com",
				Timeout:        0,
				SendClientInfo: ptrBool(false),
				IPStack:        IpStackBoth,
				u:              u2,
			},
		},
		{
			"h3",
			&UpstreamConfig{
				Name:        "doh3",
				Type:        "doh3",
				Endpoint:    "h3://example.com",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
			},
			&UpstreamConfig{
				Name:        "doh3",
				Type:        "doh3",
				Endpoint:    "https://example.com",
				BootstrapIP: "",
				Domain:      "example.com",
				Timeout:     0,
				IPStack:     IpStackBoth,
				u:           u1,
			},
		},
		{
			"h3 without type",
			&UpstreamConfig{
				Name:        "doh3",
				Endpoint:    "h3://example.com",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
			},
			&UpstreamConfig{
				Name:        "doh3",
				Type:        "doh3",
				Endpoint:    "https://example.com",
				BootstrapIP: "",
				Domain:      "example.com",
				Timeout:     0,
				IPStack:     IpStackBoth,
				u:           u1,
			},
		},
		{
			"sdns -> doh",
			&UpstreamConfig{
				Name:        "sdns",
				Type:        "sdns",
				Endpoint:    "sdns://AgMAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29tAy9wMQ",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
			&UpstreamConfig{
				Name:        "sdns",
				Type:        "doh",
				Endpoint:    "https://freedns.controld.com/p1",
				BootstrapIP: "76.76.2.11",
				Domain:      "freedns.controld.com",
				Timeout:     0,
				IPStack:     IpStackBoth,
				u:           u3,
			},
		},
		{
			"sdns -> dot",
			&UpstreamConfig{
				Name:        "sdns",
				Type:        "sdns",
				Endpoint:    "sdns://AwcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
			&UpstreamConfig{
				Name:        "sdns",
				Type:        "dot",
				Endpoint:    "freedns.controld.com:843",
				BootstrapIP: "76.76.2.11",
				Domain:      "freedns.controld.com",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
		},
		{
			"sdns -> doq",
			&UpstreamConfig{
				Name:        "sdns",
				Type:        "sdns",
				Endpoint:    "sdns://BAcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
			&UpstreamConfig{
				Name:        "sdns",
				Type:        "doq",
				Endpoint:    "freedns.controld.com:784",
				BootstrapIP: "76.76.2.11",
				Domain:      "freedns.controld.com",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
		},
		{
			"sdns -> legacy",
			&UpstreamConfig{
				Name:        "sdns",
				Type:        "sdns",
				Endpoint:    "sdns://AAcAAAAAAAAACjc2Ljc2LjIuMTE",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
			&UpstreamConfig{
				Name:        "sdns",
				Type:        "legacy",
				Endpoint:    "76.76.2.11:53",
				BootstrapIP: "76.76.2.11",
				Domain:      "76.76.2.11",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
		},
		{
			"sdns without type",
			&UpstreamConfig{
				Name:        "sdns",
				Endpoint:    "sdns://AAcAAAAAAAAACjc2Ljc2LjIuMTE",
				BootstrapIP: "",
				Domain:      "",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
			&UpstreamConfig{
				Name:        "sdns",
				Type:        "legacy",
				Endpoint:    "76.76.2.11:53",
				BootstrapIP: "76.76.2.11",
				Domain:      "76.76.2.11",
				Timeout:     0,
				IPStack:     IpStackBoth,
			},
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			tc.uc.Init(context.Background())
			tc.uc.uid = "" // we don't care about the uid.
			assert.Equal(t, tc.expected, tc.uc)
		})
	}
}

func TestUpstreamConfig_VerifyDomain(t *testing.T) {
	tests := []struct {
		name         string
		uc           *UpstreamConfig
		verifyDomain string
	}{
		{
			controlDComDomain,
			&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2"},
			controldVerifiedDomain[controlDComDomain],
		},
		{
			controlDDevDomain,
			&UpstreamConfig{Endpoint: "https://freedns.controld.dev/p2"},
			controldVerifiedDomain[controlDDevDomain],
		},
		{
			"non-ControlD upstream",
			&UpstreamConfig{Endpoint: "https://dns.google/dns-query"},
			"",
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			if got := tc.uc.VerifyDomain(); got != tc.verifyDomain {
				t.Errorf("unexpected verify domain, want: %q, got: %q", tc.verifyDomain, got)
			}
		})
	}
}

func TestUpstreamConfig_UpstreamSendClientInfo(t *testing.T) {
	tests := []struct {
		name           string
		uc             *UpstreamConfig
		sendClientInfo bool
	}{
		{
			"default with controld upstream DoH",
			&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH},
			true,
		},
		{
			"default with controld upstream DoH3",
			&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH3},
			true,
		},
		{
			"default with non-ControlD upstream",
			&UpstreamConfig{Endpoint: "https://dns.google/dns-query", Type: ResolverTypeDOH},
			false,
		},
		{
			"set false with controld upstream",
			&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH, SendClientInfo: ptrBool(false)},
			false,
		},
		{
			"set true with controld upstream",
			&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", SendClientInfo: ptrBool(true)},
			true,
		},
		{
			"set false with non-ControlD upstream",
			&UpstreamConfig{Endpoint: "https://dns.google/dns-query", SendClientInfo: ptrBool(false)},
			false,
		},
		{
			"set true with non-ControlD upstream",
			&UpstreamConfig{Endpoint: "https://dns.google/dns-query", Type: ResolverTypeDOH, SendClientInfo: ptrBool(true)},
			true,
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			if got := tc.uc.UpstreamSendClientInfo(); got != tc.sendClientInfo {
				t.Errorf("unexpected result, want: %v, got: %v", tc.sendClientInfo, got)
			}
		})
	}
}

func TestUpstreamConfig_IsDiscoverable(t *testing.T) {
	tests := []struct {
		name         string
		uc           *UpstreamConfig
		discoverable bool
	}{
		{
			"loopback",
			&UpstreamConfig{Endpoint: "127.0.0.1", Type: ResolverTypeLegacy},
			true,
		},
		{
			"rfc1918",
			&UpstreamConfig{Endpoint: "192.168.1.1", Type: ResolverTypeLegacy},
			true,
		},
		{
			"CGNAT",
			&UpstreamConfig{Endpoint: "100.66.67.68", Type: ResolverTypeLegacy},
			true,
		},
		{
			"Public IP",
			&UpstreamConfig{Endpoint: "8.8.8.8", Type: ResolverTypeLegacy},
			false,
		},
		{
			"override discoverable",
			&UpstreamConfig{Endpoint: "127.0.0.1", Type: ResolverTypeLegacy, Discoverable: ptrBool(false)},
			false,
		},
		{
			"override non-public",
			&UpstreamConfig{Endpoint: "1.1.1.1", Type: ResolverTypeLegacy, Discoverable: ptrBool(true)},
			true,
		},
		{
			"non-legacy upstream",
			&UpstreamConfig{Endpoint: "https://192.168.1.1/custom-doh", Type: ResolverTypeDOH},
			false,
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			tc.uc.Init(context.Background())
			if got := tc.uc.IsDiscoverable(); got != tc.discoverable {
				t.Errorf("unexpected result, want: %v, got: %v", tc.discoverable, got)
			}
		})
	}
}

func TestRebootstrapRace(t *testing.T) {
	uc := &UpstreamConfig{
		Name:         "test-doh",
		Type:         ResolverTypeDOH,
		Endpoint:     "https://example.com/dns-query",
		Domain:       "example.com",
		bootstrapIPs: []string{"1.1.1.1", "1.0.0.1"},
	}

	ctx := LoggerCtx(context.Background(), NopLogger)

	uc.SetupTransport(ctx)

	if uc.transport == nil {
		t.Fatal("initial transport should be set")
	}

	const goroutines = 100

	uc.ReBootstrap(ctx)

	started := make(chan struct{})
	go func() {
		close(started)
		for {
			switch uc.rebootstrap.Load() {
			case rebootstrapStarted, rebootstrapInProgress:
				uc.ReBootstrap(ctx)
			default:
				return
			}
		}
	}()

	<-started

	var wg sync.WaitGroup
	for range goroutines {
		wg.Go(func() {
			uc.ensureSetupTransport(ctx)
		})
	}

	wg.Wait()
}

func ptrBool(b bool) *bool {
	return &b
}
