package dnsforward

import (
	"crypto/tls"
	"net"
	"net/http"
	"net/url"
	"testing"

	"github.com/AdguardTeam/dnsproxy/proxy"
	"github.com/AdguardTeam/golibs/testutil"
	"github.com/stretchr/testify/assert"
)

// testTLSConn is a tlsConn for tests.
type testTLSConn struct {
	// Conn is embedded here simply to make testTLSConn a net.Conn without
	// actually implementing all methods.
	net.Conn

	serverName string
}

// ConnectionState implements the tlsConn interface for testTLSConn.
func (c testTLSConn) ConnectionState() (cs tls.ConnectionState) {
	cs.ServerName = c.serverName

	return cs
}

func TestServer_clientIDFromDNSContext(t *testing.T) {
	testCases := []struct {
		name         string
		proto        proxy.Proto
		confSrvName  string
		cliSrvName   string
		wantClientID string
		wantErrMsg   string
		inclHTTPTLS  bool
		strictSNI    bool
	}{{
		name:         "udp",
		proto:        proxy.ProtoUDP,
		confSrvName:  "",
		cliSrvName:   "",
		wantClientID: "",
		wantErrMsg:   "",
		inclHTTPTLS:  false,
		strictSNI:    false,
	}, {
		name:         "tls_no_clientid",
		proto:        proxy.ProtoTLS,
		confSrvName:  "example.com",
		cliSrvName:   "example.com",
		wantClientID: "",
		wantErrMsg:   "",
		inclHTTPTLS:  false,
		strictSNI:    true,
	}, {
		name:         "tls_no_client_server_name",
		proto:        proxy.ProtoTLS,
		confSrvName:  "example.com",
		cliSrvName:   "",
		wantClientID: "",
		wantErrMsg: `clientid check: client server name "" ` +
			`doesn't match host server name "example.com"`,
		inclHTTPTLS: false,
		strictSNI:   true,
	}, {
		name:         "tls_no_client_server_name_no_strict",
		proto:        proxy.ProtoTLS,
		confSrvName:  "example.com",
		cliSrvName:   "",
		wantClientID: "",
		wantErrMsg:   "",
		inclHTTPTLS:  false,
		strictSNI:    false,
	}, {
		name:         "tls_clientid",
		proto:        proxy.ProtoTLS,
		confSrvName:  "example.com",
		cliSrvName:   "cli.example.com",
		wantClientID: "cli",
		wantErrMsg:   "",
		inclHTTPTLS:  false,
		strictSNI:    true,
	}, {
		name:         "tls_clientid_hostname_error",
		proto:        proxy.ProtoTLS,
		confSrvName:  "example.com",
		cliSrvName:   "cli.example.net",
		wantClientID: "",
		wantErrMsg: `clientid check: client server name "cli.example.net" ` +
			`doesn't match host server name "example.com"`,
		inclHTTPTLS: false,
		strictSNI:   true,
	}, {
		name:         "tls_invalid_clientid",
		proto:        proxy.ProtoTLS,
		confSrvName:  "example.com",
		cliSrvName:   "!!!.example.com",
		wantClientID: "",
		wantErrMsg: `clientid check: invalid clientid "!!!": ` +
			`bad hostname label rune '!'`,
		inclHTTPTLS: false,
		strictSNI:   true,
	}, {
		name:        "tls_clientid_too_long",
		proto:       proxy.ProtoTLS,
		confSrvName: "example.com",
		cliSrvName: `abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmno` +
			`pqrstuvwxyz0123456789.example.com`,
		wantClientID: "",
		wantErrMsg: `clientid check: invalid clientid "abcdefghijklmno` +
			`pqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789": ` +
			`hostname label is too long: got 72, max 63`,
		inclHTTPTLS: false,
		strictSNI:   true,
	}, {
		name:         "quic_clientid",
		proto:        proxy.ProtoQUIC,
		confSrvName:  "example.com",
		cliSrvName:   "cli.example.com",
		wantClientID: "cli",
		wantErrMsg:   "",
		inclHTTPTLS:  false,
		strictSNI:    true,
	}, {
		name:         "tls_clientid_issue3437",
		proto:        proxy.ProtoTLS,
		confSrvName:  "example.com",
		cliSrvName:   "cli.myexample.com",
		wantClientID: "",
		wantErrMsg: `clientid check: client server name "cli.myexample.com" ` +
			`doesn't match host server name "example.com"`,
		inclHTTPTLS: false,
		strictSNI:   true,
	}, {
		name:         "tls_case",
		proto:        proxy.ProtoTLS,
		confSrvName:  "example.com",
		cliSrvName:   "InSeNsItIvE.example.com",
		wantClientID: "insensitive",
		wantErrMsg:   ``,
		inclHTTPTLS:  false,
		strictSNI:    true,
	}, {
		name:         "quic_case",
		proto:        proxy.ProtoQUIC,
		confSrvName:  "example.com",
		cliSrvName:   "InSeNsItIvE.example.com",
		wantClientID: "insensitive",
		wantErrMsg:   ``,
		inclHTTPTLS:  false,
		strictSNI:    true,
	}, {
		name:         "https_no_clientid",
		proto:        proxy.ProtoHTTPS,
		confSrvName:  "example.com",
		cliSrvName:   "example.com",
		wantClientID: "",
		wantErrMsg:   "",
		inclHTTPTLS:  true,
		strictSNI:    true,
	}, {
		name:         "https_clientid",
		proto:        proxy.ProtoHTTPS,
		confSrvName:  "example.com",
		cliSrvName:   "cli.example.com",
		wantClientID: "cli",
		wantErrMsg:   "",
		inclHTTPTLS:  true,
		strictSNI:    true,
	}, {
		name:         "https_issue5518",
		proto:        proxy.ProtoHTTPS,
		confSrvName:  "example.com",
		cliSrvName:   "cli.example.com",
		wantClientID: "cli",
		wantErrMsg:   "",
		inclHTTPTLS:  false,
		strictSNI:    true,
	}, {
		name:         "https_no_host",
		proto:        proxy.ProtoHTTPS,
		confSrvName:  "example.com",
		cliSrvName:   "example.com",
		wantClientID: "",
		wantErrMsg:   "",
		inclHTTPTLS:  false,
		strictSNI:    true,
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			tlsConf := &TLSConfig{
				ServerName:     tc.confSrvName,
				StrictSNICheck: tc.strictSNI,
			}

			srv := &Server{
				conf:       ServerConfig{TLSConf: tlsConf},
				baseLogger: testLogger,
				logger:     testLogger,
			}

			var (
				conn    net.Conn
				httpReq *http.Request
			)

			switch tc.proto {
			case proxy.ProtoHTTPS:
				httpReq = newHTTPReq(tc.cliSrvName, tc.inclHTTPTLS)
			case proxy.ProtoQUIC:
				// TODO(a.garipov):  Find ways of testing this with the new
				// quic-go API.
				t.Skipf("skipped during the quic-go api update")
			case proxy.ProtoTLS:
				conn = testTLSConn{
					serverName: tc.cliSrvName,
				}
			}

			pctx := &proxy.DNSContext{
				Proto:       tc.proto,
				Conn:        conn,
				HTTPRequest: httpReq,
			}

			ctx := testutil.ContextWithTimeout(t, testTimeout)
			clientID, err := srv.clientIDFromDNSContext(ctx, testLogger, pctx)
			assert.Equal(t, tc.wantClientID, clientID)

			testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
		})
	}
}

// newHTTPReq is a helper to create HTTP requests for tests.
func newHTTPReq(cliSrvName string, inclTLS bool) (r *http.Request) {
	u := &url.URL{
		Path: "/dns-query",
	}

	r = &http.Request{
		ProtoMajor: 1,
		ProtoMinor: 1,
		URL:        u,
		Host:       cliSrvName,
	}

	if inclTLS {
		r.TLS = &tls.ConnectionState{
			ServerName: cliSrvName,
		}
	}

	return r
}

func TestClientIDFromDNSContextHTTPS(t *testing.T) {
	testCases := []struct {
		clientID     string
		name         string
		pattern      string
		cliSrvName   string
		wantClientID string
		wantErrMsg   string
	}{{
		clientID:     "",
		name:         "no_clientid",
		pattern:      "GET /dns-query",
		cliSrvName:   "example.com",
		wantClientID: "",
		wantErrMsg:   "",
	}, {
		clientID:     "",
		name:         "no_clientid_slash",
		pattern:      "GET /dns-query/",
		cliSrvName:   "example.com",
		wantClientID: "",
		wantErrMsg:   "",
	}, {
		clientID:     "cli",
		name:         "clientid",
		pattern:      "GET /dns-query/{ClientID}",
		cliSrvName:   "example.com",
		wantClientID: "cli",
		wantErrMsg:   "",
	}, {
		clientID:     "cli",
		name:         "clientid_slash",
		pattern:      "GET /dns-query/{ClientID}/",
		cliSrvName:   "example.com",
		wantClientID: "cli",
		wantErrMsg:   "",
	}, {
		clientID:     "InSeNsItIvE",
		name:         "clientid_case",
		pattern:      "GET /dns-query/{ClientID}",
		cliSrvName:   "example.com",
		wantClientID: "insensitive",
		wantErrMsg:   ``,
	}, {
		clientID:     "!!!",
		name:         "invalid_clientid",
		pattern:      "GET /dns-query/{ClientID}",
		cliSrvName:   "example.com",
		wantClientID: "",
		wantErrMsg:   `clientid check: invalid clientid "!!!": bad hostname label rune '!'`,
	}, {
		clientID:     "right",
		name:         "both_ids",
		pattern:      "GET /dns-query/{ClientID}",
		cliSrvName:   "wrong.example.com",
		wantClientID: "right",
		wantErrMsg:   "",
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			connState := &tls.ConnectionState{
				ServerName: tc.cliSrvName,
			}

			r := &http.Request{
				TLS:     connState,
				Pattern: tc.pattern,
			}
			r.SetPathValue("ClientID", tc.clientID)

			pctx := &proxy.DNSContext{
				Proto:       proxy.ProtoHTTPS,
				HTTPRequest: r,
			}

			clientID, err := clientIDFromDNSContextHTTPS(pctx)
			assert.Equal(t, tc.wantClientID, clientID)
			testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
		})
	}
}
