package netclient

import (
	"bufio"
	"context"
	"crypto/tls"
	"encoding/binary"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"sync/atomic"
	"testing"
	"time"
)

func TestCustomProxyBuildsSocks5URL(t *testing.T) {
	pf, err := proxyFunc(ProxySpec{
		Mode:     "custom",
		Type:     "socks5",
		Server:   "127.0.0.1",
		Port:     7890,
		Username: "user",
		Password: "secret",
	})
	if err != nil {
		t.Fatalf("proxyFunc: %v", err)
	}
	got, err := pf(&http.Request{URL: mustURL("https://api.deepseek.com/chat/completions")})
	if err != nil {
		t.Fatalf("proxy lookup: %v", err)
	}
	if got.Scheme != "socks5" || got.Host != "127.0.0.1:7890" {
		t.Fatalf("proxy URL = %s, want socks5://127.0.0.1:7890", got)
	}
	if pass, ok := got.User.Password(); !ok || pass != "secret" {
		t.Fatalf("proxy password not preserved")
	}
}

func TestCustomProxyHonorsNoProxy(t *testing.T) {
	pf, err := proxyFunc(ProxySpec{
		Mode:    "custom",
		URL:     "http://proxy.example.com:8080",
		NoProxy: "api.deepseek.com",
	})
	if err != nil {
		t.Fatalf("proxyFunc: %v", err)
	}
	got, err := pf(&http.Request{URL: mustURL("https://api.deepseek.com/chat/completions")})
	if err != nil {
		t.Fatalf("proxy lookup: %v", err)
	}
	if got != nil {
		t.Fatalf("NoProxy host should bypass proxy, got %s", got)
	}
}

func TestDirectHostsBypassProxy(t *testing.T) {
	t.Setenv("HTTPS_PROXY", "http://proxy.example.com:8080")
	t.Setenv("NO_PROXY", "")
	pf, err := proxyFunc(ProxySpec{Mode: "auto", DirectHosts: []string{"token-plan-cn.xiaomimimo.com"}})
	if err != nil {
		t.Fatalf("proxyFunc: %v", err)
	}

	got, err := pf(&http.Request{URL: mustURL("https://token-plan-cn.xiaomimimo.com/v1/chat")})
	if err != nil {
		t.Fatalf("direct-host lookup: %v", err)
	}
	if got != nil {
		t.Fatalf("a direct host should bypass the proxy, got %s", got)
	}

	other, err := pf(&http.Request{URL: mustURL("https://example.com/x")})
	if err != nil {
		t.Fatalf("other lookup: %v", err)
	}
	if other == nil || other.Host != "proxy.example.com:8080" {
		t.Fatalf("non-direct host should still use the env proxy, got %v", other)
	}
}

func TestNoDirectHostsKeepsEveryoneProxied(t *testing.T) {
	t.Setenv("HTTPS_PROXY", "http://proxy.example.com:8080")
	t.Setenv("NO_PROXY", "")
	pf, err := proxyFunc(ProxySpec{Mode: "env"}) // no DirectHosts → nothing special-cased
	if err != nil {
		t.Fatalf("proxyFunc: %v", err)
	}
	got, err := pf(&http.Request{URL: mustURL("https://token-plan-cn.xiaomimimo.com/v1/chat")})
	if err != nil {
		t.Fatalf("lookup: %v", err)
	}
	if got == nil || got.Host != "proxy.example.com:8080" {
		t.Fatalf("without DirectHosts the host must go through the proxy, got %v", got)
	}
}

func TestOffProxyDisablesProxy(t *testing.T) {
	pf, err := proxyFunc(ProxySpec{Mode: "off"})
	if err != nil {
		t.Fatalf("proxyFunc: %v", err)
	}
	if pf != nil {
		t.Fatal("off mode should return nil proxy func")
	}
}

func TestSummaryRedactsPassword(t *testing.T) {
	got := Summary(ProxySpec{
		Mode:     "custom",
		Type:     "socks5",
		Server:   "proxy.example.com",
		Port:     1080,
		Username: "user",
		Password: "secret",
	})
	if got != "custom (socks5://user@proxy.example.com:1080)" {
		t.Fatalf("Summary = %q", got)
	}
}

func TestHTTPClientProxyModesAffectRequests(t *testing.T) {
	var targetHits atomic.Int32
	target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		targetHits.Add(1)
		_, _ = io.WriteString(w, "target")
	}))
	t.Cleanup(target.Close)
	targetAddr := strings.TrimPrefix(target.URL, "http://")

	var envProxyHits atomic.Int32
	envProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		envProxyHits.Add(1)
		if got, want := r.URL.String(), "http://service.test/resource"; got != want {
			t.Errorf("env proxy request URL = %q, want %q", got, want)
		}
		_, _ = io.WriteString(w, "env-proxy")
	}))
	t.Cleanup(envProxy.Close)

	var customProxyHits atomic.Int32
	customProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		customProxyHits.Add(1)
		if got, want := r.URL.String(), "http://service.test/resource"; got != want {
			t.Errorf("custom proxy request URL = %q, want %q", got, want)
		}
		_, _ = io.WriteString(w, "custom-proxy")
	}))
	t.Cleanup(customProxy.Close)

	// Windows env vars are case-insensitive, so HTTP_PROXY and http_proxy are the
	// same var — set the intended value last or the empty clear wipes it.
	t.Setenv("http_proxy", "")
	t.Setenv("HTTPS_PROXY", "")
	t.Setenv("https_proxy", "")
	t.Setenv("NO_PROXY", "")
	t.Setenv("no_proxy", "")
	t.Setenv("HTTP_PROXY", envProxy.URL)

	tests := []struct {
		name           string
		spec           ProxySpec
		wantBody       string
		wantTargetHits int32
		wantEnvHits    int32
		wantCustomHits int32
	}{
		{
			name:        "auto uses environment proxy",
			spec:        ProxySpec{Mode: ModeAuto},
			wantBody:    "env-proxy",
			wantEnvHits: 1,
		},
		{
			name:        "env uses environment proxy",
			spec:        ProxySpec{Mode: ModeEnv},
			wantBody:    "env-proxy",
			wantEnvHits: 1,
		},
		{
			name:           "custom ignores environment proxy",
			spec:           ProxySpec{Mode: ModeCustom, URL: customProxy.URL},
			wantBody:       "custom-proxy",
			wantCustomHits: 1,
		},
		{
			name:           "custom no_proxy bypasses proxy",
			spec:           ProxySpec{Mode: ModeCustom, URL: customProxy.URL, NoProxy: "service.test"},
			wantBody:       "target",
			wantTargetHits: 1,
		},
		{
			name:           "off bypasses environment proxy",
			spec:           ProxySpec{Mode: ModeOff},
			wantBody:       "target",
			wantTargetHits: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			targetHits.Store(0)
			envProxyHits.Store(0)
			customProxyHits.Store(0)

			client := mappedClient(t, tt.spec, "service.test:80", targetAddr)
			resp, err := client.Get("http://service.test/resource")
			if err != nil {
				t.Fatalf("GET: %v", err)
			}
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				t.Fatalf("read body: %v", err)
			}
			if string(body) != tt.wantBody {
				t.Fatalf("body = %q, want %q", body, tt.wantBody)
			}
			if got := targetHits.Load(); got != tt.wantTargetHits {
				t.Fatalf("target hits = %d, want %d", got, tt.wantTargetHits)
			}
			if got := envProxyHits.Load(); got != tt.wantEnvHits {
				t.Fatalf("env proxy hits = %d, want %d", got, tt.wantEnvHits)
			}
			if got := customProxyHits.Load(); got != tt.wantCustomHits {
				t.Fatalf("custom proxy hits = %d, want %d", got, tt.wantCustomHits)
			}
		})
	}
}

func TestStructuredProxyTypesAffectRequests(t *testing.T) {
	httpProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got, want := r.URL.String(), "http://service.test/resource"; got != want {
			t.Errorf("HTTP proxy request URL = %q, want %q", got, want)
		}
		_, _ = io.WriteString(w, "http-proxy")
	}))
	t.Cleanup(httpProxy.Close)

	httpsProxy := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got, want := r.URL.String(), "http://service.test/resource"; got != want {
			t.Errorf("HTTPS proxy request URL = %q, want %q", got, want)
		}
		_, _ = io.WriteString(w, "https-proxy")
	}))
	t.Cleanup(httpsProxy.Close)

	socks5Proxy := newSocksHTTPProxy(t)
	socks5hProxy := newSocksHTTPProxy(t)

	tests := []struct {
		name     string
		spec     ProxySpec
		tlsProxy *httptest.Server
		wantBody string
	}{
		{
			name:     "http",
			spec:     structuredProxySpec(t, "http", httpProxy.URL),
			wantBody: "http-proxy",
		},
		{
			name:     "https",
			spec:     structuredProxySpec(t, "https", httpsProxy.URL),
			tlsProxy: httpsProxy,
			wantBody: "https-proxy",
		},
		{
			name:     "socks5",
			spec:     structuredProxySpec(t, "socks5", "http://"+socks5Proxy.addr),
			wantBody: "socks-proxy",
		},
		{
			name:     "socks5h",
			spec:     structuredProxySpec(t, "socks5h", "http://"+socks5hProxy.addr),
			wantBody: "socks-proxy",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tr := mappedTransport(t, tt.spec, "service.test:80", "127.0.0.1:1")
			if tt.tlsProxy != nil {
				proxyTransport := tt.tlsProxy.Client().Transport.(*http.Transport)
				tr.TLSClientConfig = proxyTransport.TLSClientConfig
			}
			client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
			resp, err := client.Get("http://service.test/resource")
			if err != nil {
				t.Fatalf("GET: %v", err)
			}
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				t.Fatalf("read body: %v", err)
			}
			if string(body) != tt.wantBody {
				t.Fatalf("body = %q, want %q", body, tt.wantBody)
			}
		})
	}

	if got := socks5Proxy.hits.Load(); got != 1 {
		t.Fatalf("socks5 proxy hits = %d, want 1", got)
	}
	if got := socks5hProxy.hits.Load(); got != 1 {
		t.Fatalf("socks5h proxy hits = %d, want 1", got)
	}
}

func TestHTTPSRequestsRespectProxyModes(t *testing.T) {
	var targetHits atomic.Int32
	target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		targetHits.Add(1)
		w.Header().Set("Connection", "close")
		_, _ = io.WriteString(w, "https-target")
	}))
	t.Cleanup(target.Close)
	targetAddr := strings.TrimPrefix(target.URL, "https://")

	var proxyHits int32
	proxy := newConnectProxy(t, targetAddr, &proxyHits)
	t.Cleanup(proxy.Close)

	// Set HTTPS_PROXY last: on Windows it and https_proxy are the same var.
	t.Setenv("HTTP_PROXY", "")
	t.Setenv("http_proxy", "")
	t.Setenv("https_proxy", "")
	t.Setenv("NO_PROXY", "")
	t.Setenv("no_proxy", "")
	t.Setenv("HTTPS_PROXY", proxy.URL)

	tests := []struct {
		name          string
		spec          ProxySpec
		wantProxyHits int32
	}{
		{
			name:          "auto uses HTTPS_PROXY",
			spec:          ProxySpec{Mode: ModeAuto},
			wantProxyHits: 1,
		},
		{
			name:          "custom proxies HTTPS requests",
			spec:          ProxySpec{Mode: ModeCustom, URL: proxy.URL},
			wantProxyHits: 1,
		},
		{
			name: "custom no_proxy bypasses HTTPS proxy",
			spec: ProxySpec{Mode: ModeCustom, URL: proxy.URL, NoProxy: "service.test"},
		},
		{
			name: "off bypasses HTTPS_PROXY",
			spec: ProxySpec{Mode: ModeOff},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			targetHits.Store(0)
			atomic.StoreInt32(&proxyHits, 0)

			tr := mappedTransport(t, tt.spec, "service.test:443", targetAddr)
			tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
			client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
			resp, err := client.Get("https://service.test/resource")
			if err != nil {
				t.Fatalf("GET: %v", err)
			}
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				t.Fatalf("read body: %v", err)
			}
			if string(body) != "https-target" {
				t.Fatalf("body = %q, want https-target", body)
			}
			if got := targetHits.Load(); got != 1 {
				t.Fatalf("target hits = %d, want 1", got)
			}
			if got := atomic.LoadInt32(&proxyHits); got != tt.wantProxyHits {
				t.Fatalf("CONNECT proxy hits = %d, want %d", got, tt.wantProxyHits)
			}
		})
	}
}

func structuredProxySpec(t *testing.T, typ, rawURL string) ProxySpec {
	t.Helper()
	u, err := url.Parse(rawURL)
	if err != nil {
		t.Fatalf("parse proxy URL: %v", err)
	}
	host, portText, err := net.SplitHostPort(u.Host)
	if err != nil {
		t.Fatalf("split proxy host: %v", err)
	}
	port, err := strconv.Atoi(portText)
	if err != nil {
		t.Fatalf("parse proxy port: %v", err)
	}
	return ProxySpec{Mode: ModeCustom, Type: typ, Server: host, Port: port}
}

func mappedClient(t *testing.T, spec ProxySpec, fromAddr, toAddr string) *http.Client {
	t.Helper()
	tr := mappedTransport(t, spec, fromAddr, toAddr)
	return &http.Client{Transport: tr, Timeout: 2 * time.Second}
}

func mappedTransport(t *testing.T, spec ProxySpec, fromAddr, toAddr string) *http.Transport {
	t.Helper()
	tr, err := NewTransport(spec, TransportOptions{})
	if err != nil {
		t.Fatalf("NewTransport: %v", err)
	}
	t.Cleanup(tr.CloseIdleConnections)
	dialer := &net.Dialer{Timeout: time.Second}
	tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
		if addr == fromAddr {
			addr = toAddr
		}
		return dialer.DialContext(ctx, network, addr)
	}
	return tr
}

type socksHTTPProxy struct {
	addr string
	hits atomic.Int32
}

func newSocksHTTPProxy(t *testing.T) *socksHTTPProxy {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen socks proxy: %v", err)
	}
	p := &socksHTTPProxy{addr: ln.Addr().String()}
	t.Cleanup(func() { _ = ln.Close() })
	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				return
			}
			go p.handle(conn)
		}
	}()
	return p
}

func (p *socksHTTPProxy) handle(conn net.Conn) {
	defer conn.Close()
	_ = conn.SetDeadline(time.Now().Add(2 * time.Second))
	r := bufio.NewReader(conn)

	header := make([]byte, 2)
	if _, err := io.ReadFull(r, header); err != nil || header[0] != 5 {
		return
	}
	methods := make([]byte, int(header[1]))
	if _, err := io.ReadFull(r, methods); err != nil {
		return
	}
	if _, err := conn.Write([]byte{5, 0}); err != nil {
		return
	}

	req := make([]byte, 4)
	if _, err := io.ReadFull(r, req); err != nil || req[0] != 5 || req[1] != 1 {
		return
	}
	if !p.readSocksAddr(r, req[3]) {
		return
	}
	p.hits.Add(1)
	if _, err := conn.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}); err != nil {
		return
	}

	httpReq, err := http.ReadRequest(r)
	if err != nil {
		return
	}
	_ = httpReq.Body.Close()
	_, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 11\r\nConnection: close\r\n\r\nsocks-proxy"))
}

func (p *socksHTTPProxy) readSocksAddr(r *bufio.Reader, atyp byte) bool {
	switch atyp {
	case 1:
		if _, err := io.ReadFull(r, make([]byte, net.IPv4len)); err != nil {
			return false
		}
	case 3:
		size, err := r.ReadByte()
		if err != nil {
			return false
		}
		if _, err := io.ReadFull(r, make([]byte, int(size))); err != nil {
			return false
		}
	case 4:
		if _, err := io.ReadFull(r, make([]byte, net.IPv6len)); err != nil {
			return false
		}
	default:
		return false
	}
	port := make([]byte, 2)
	if _, err := io.ReadFull(r, port); err != nil {
		return false
	}
	return binary.BigEndian.Uint16(port) != 0
}

func newConnectProxy(t *testing.T, targetAddr string, hits *int32) *httptest.Server {
	t.Helper()
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodConnect {
			t.Errorf("proxy method = %s, want CONNECT", r.Method)
			http.Error(w, "CONNECT required", http.StatusMethodNotAllowed)
			return
		}
		atomic.AddInt32(hits, 1)
		clientConn, _, err := http.NewResponseController(w).Hijack()
		if err != nil {
			t.Errorf("hijack CONNECT: %v", err)
			return
		}
		defer clientConn.Close()
		targetConn, err := net.DialTimeout("tcp", targetAddr, time.Second)
		if err != nil {
			t.Errorf("dial CONNECT target: %v", err)
			return
		}
		defer targetConn.Close()
		if _, err := clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")); err != nil {
			return
		}
		go func() {
			_, _ = io.Copy(targetConn, clientConn)
		}()
		_, _ = io.Copy(clientConn, targetConn)
	}))
}

func mustURL(s string) *url.URL {
	u, err := url.Parse(s)
	if err != nil {
		panic(err)
	}
	return u
}

func TestForceIPv4Dials(t *testing.T) {
	tr, err := NewTransport(ProxySpec{Mode: ModeOff}, TransportOptions{ForceIPv4: true})
	if err != nil {
		t.Fatal(err)
	}
	if tr.DialContext == nil {
		t.Fatal("ForceIPv4 should install a DialContext")
	}
	ln, err := net.Listen("tcp4", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()

	conn, err := tr.DialContext(context.Background(), "tcp", ln.Addr().String())
	if err != nil {
		t.Fatalf("forced-IPv4 dial to a v4 listener failed: %v", err)
	}
	conn.Close()

	// A tcp4 dialer rejects an IPv6 literal outright (address-family mismatch),
	// which is exactly the IPv6 route the fallback is meant to skip.
	if c, err := tr.DialContext(context.Background(), "tcp", "[::1]:9"); err == nil {
		c.Close()
		t.Error("forced-IPv4 dial should reject an IPv6 address")
	}
}
