package runtime

import (
	"context"
	"crypto/sha256"
	"crypto/tls"
	"encoding/base64"
	"encoding/hex"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"testing"

	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
	"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
)

type generationProbeTransport struct {
	id     string
	closed atomic.Int32
}

func (t *generationProbeTransport) RoundTrip(*http.Request) (*http.Response, error) {
	return &http.Response{
		StatusCode: http.StatusOK,
		Body:       http.NoBody,
		Header:     make(http.Header),
		Request:    &http.Request{},
	}, nil
}

func (t *generationProbeTransport) CloseIdleConnections() {
	t.closed.Add(1)
}

func TestCredentialRotatingTransportDropsOldPoolBeforeNextRequest(t *testing.T) {
	var selected atomic.Pointer[generationProbeTransport]
	oldTransport := &generationProbeTransport{id: "old"}
	newTransport := &generationProbeTransport{id: "new"}
	selected.Store(oldTransport)

	rotating, err := newCredentialRotatingTransport(func() (idleClosingRoundTripper, error) {
		return selected.Load(), nil
	})
	if err != nil {
		t.Fatalf("newCredentialRotatingTransport: %v", err)
	}
	rotating.mu.Lock()
	initial := rotating.current
	rotating.mu.Unlock()
	if initial != oldTransport {
		t.Fatalf("initial transport = %p, want old %p", initial, oldTransport)
	}

	selected.Store(newTransport)
	InvalidateMasterClientConnections()

	req := httptest.NewRequest(http.MethodGet, "https://node.example.test/panel/api/server/status", nil)
	resp, err := rotating.RoundTrip(req)
	if err != nil {
		t.Fatalf("RoundTrip after credential rotation: %v", err)
	}
	_ = resp.Body.Close()

	rotating.mu.Lock()
	current := rotating.current
	rotating.mu.Unlock()
	if current != newTransport {
		t.Fatalf("transport after invalidation = %p, want new %p", current, newTransport)
	}
	if got := oldTransport.closed.Load(); got != 1 {
		t.Fatalf("old transport CloseIdleConnections calls = %d, want 1", got)
	}
}

func TestReloadMasterClientConnectionsValidatesProviderBeforeInvalidation(t *testing.T) {
	before := masterCertEpoch.Load()
	SetMasterClientCertProvider(func() (tls.Certificate, error) {
		return tls.Certificate{}, context.Canceled
	})
	if err := ReloadMasterClientConnections(); err == nil {
		t.Fatal("reload with an invalid provider unexpectedly succeeded")
	}
	if got := masterCertEpoch.Load(); got != before {
		t.Fatalf("failed reload changed generation from %d to %d", before, got)
	}

	SetMasterClientCertProvider(func() (tls.Certificate, error) {
		return masterCertForTest(t), nil
	})
	t.Cleanup(func() { SetMasterClientCertProvider(nil) })
	if err := ReloadMasterClientConnections(); err != nil {
		t.Fatalf("ReloadMasterClientConnections: %v", err)
	}
	if got := masterCertEpoch.Load(); got != before+1 {
		t.Fatalf("successful reload generation = %d, want %d", got, before+1)
	}
}

func TestCredentialRotatingTransportRejectsBuildAcrossInvalidation(t *testing.T) {
	oldTransport := &generationProbeTransport{id: "old"}
	newTransport := &generationProbeTransport{id: "new"}
	var selected atomic.Pointer[generationProbeTransport]
	selected.Store(oldTransport)

	firstBuildCaptured := make(chan struct{})
	releaseFirstBuild := make(chan struct{})
	var once sync.Once
	build := func() (idleClosingRoundTripper, error) {
		captured := selected.Load()
		once.Do(func() {
			close(firstBuildCaptured)
			<-releaseFirstBuild
		})
		return captured, nil
	}

	type result struct {
		transport *credentialRotatingTransport
		err       error
	}
	resultCh := make(chan result, 1)
	go func() {
		transport, err := newCredentialRotatingTransport(build)
		resultCh <- result{transport: transport, err: err}
	}()

	<-firstBuildCaptured
	selected.Store(newTransport)
	InvalidateMasterClientConnections()
	close(releaseFirstBuild)

	got := <-resultCh
	if got.err != nil {
		t.Fatalf("newCredentialRotatingTransport: %v", got.err)
	}
	got.transport.mu.Lock()
	current := got.transport.current
	got.transport.mu.Unlock()
	if current != newTransport {
		t.Fatalf("transport built across invalidation = %p, want new %p", current, newTransport)
	}
	if calls := oldTransport.closed.Load(); calls != 1 {
		t.Fatalf("stale transport CloseIdleConnections calls = %d, want 1", calls)
	}
}

func TestHTTPClientForNodeMTLSRebuildsTLSConfigAfterCredentialInvalidation(t *testing.T) {
	oldCert := masterCertForTest(t)
	newCert := masterCertForTest(t)
	selected := oldCert
	SetMasterClientCertProvider(func() (tls.Certificate, error) { return selected, nil })
	t.Cleanup(func() { SetMasterClientCertProvider(nil) })

	client, err := HTTPClientForNode(&model.Node{
		Scheme:        "https",
		Address:       "node.example.test",
		Port:          443,
		TlsVerifyMode: "mtls",
	}, "")
	if err != nil {
		t.Fatalf("HTTPClientForNode: %v", err)
	}
	rotating, ok := client.Transport.(*credentialRotatingTransport)
	if !ok {
		t.Fatalf("transport = %T, want *credentialRotatingTransport", client.Transport)
	}
	leaf := func() []byte {
		rotating.mu.Lock()
		defer rotating.mu.Unlock()
		transport, ok := rotating.current.(*http.Transport)
		if !ok {
			t.Fatalf("current transport = %T, want *http.Transport", rotating.current)
		}
		return transport.TLSClientConfig.Certificates[0].Certificate[0]
	}
	if got := leaf(); string(got) != string(oldCert.Certificate[0]) {
		t.Fatal("initial TLS config does not contain the old credential")
	}

	selected = newCert
	InvalidateMasterClientConnections()

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://node.example.test/", nil)
	if err != nil {
		t.Fatalf("NewRequestWithContext: %v", err)
	}
	if _, err := client.Do(req); err == nil {
		t.Fatal("canceled request unexpectedly succeeded")
	}
	if got := leaf(); string(got) != string(newCert.Certificate[0]) {
		t.Fatal("TLS config retained the old credential after invalidation")
	}
}

func TestHTTPClientForNodeProxyMTLSRebuildKeepsProxyAndNewCredential(t *testing.T) {
	oldCert := masterCertForTest(t)
	newCert := masterCertForTest(t)
	selected := oldCert
	SetMasterClientCertProvider(func() (tls.Certificate, error) { return selected, nil })
	t.Cleanup(func() { SetMasterClientCertProvider(nil) })

	const proxyURL = "http://127.0.0.1:18080"
	client, err := HTTPClientForNode(&model.Node{Scheme: "https", TlsVerifyMode: "mtls"}, proxyURL)
	if err != nil {
		t.Fatalf("HTTPClientForNode: %v", err)
	}
	rotating, ok := client.Transport.(*credentialRotatingTransport)
	if !ok {
		t.Fatalf("transport = %T, want rotating transport", client.Transport)
	}
	current := func() *http.Transport {
		rotating.mu.Lock()
		defer rotating.mu.Unlock()
		transport, ok := rotating.current.(*http.Transport)
		if !ok {
			t.Fatalf("current transport = %T, want *http.Transport", rotating.current)
		}
		return transport
	}
	assertProxy := func(transport *http.Transport) {
		t.Helper()
		if transport.Proxy == nil {
			t.Fatalf("proxy function is nil, want %s", proxyURL)
		}
		req, _ := http.NewRequest(http.MethodGet, "https://node.example.test/", nil)
		got, err := transport.Proxy(req)
		if err != nil || got == nil || got.String() != proxyURL {
			t.Fatalf("proxy = %v, error = %v, want %s", got, err, proxyURL)
		}
	}
	assertProxy(current())

	selected = newCert
	InvalidateMasterClientConnections()
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://node.example.test/", nil)
	_, _ = client.Do(req)
	rebuilt := current()
	assertProxy(rebuilt)
	if got := rebuilt.TLSClientConfig.Certificates[0].Certificate[0]; string(got) != string(newCert.Certificate[0]) {
		t.Fatal("proxy mTLS rebuild retained the old credential")
	}
}

// masterCertForTest builds a real CA-signed client certificate for mtls tests.
func masterCertForTest(t *testing.T) tls.Certificate {
	t.Helper()
	ca, err := crypto.GenerateNodeCA("test ca")
	if err != nil {
		t.Fatalf("GenerateNodeCA: %v", err)
	}
	client, err := crypto.IssueClientCert(ca, "master")
	if err != nil {
		t.Fatalf("IssueClientCert: %v", err)
	}
	cert, err := tls.X509KeyPair(client.CertPEM, client.KeyPEM)
	if err != nil {
		t.Fatalf("X509KeyPair: %v", err)
	}
	return cert
}

// TestTLSConfigForNode_MTLS_PresentsClientCert asserts the mtls branch presents
// the master client cert and verifies the node's server cert against system
// roots (no InsecureSkipVerify, no custom RootCAs).
func TestTLSConfigForNode_MTLS_PresentsClientCert(t *testing.T) {
	cert := masterCertForTest(t)
	SetMasterClientCertProvider(func() (tls.Certificate, error) { return cert, nil })
	t.Cleanup(func() { SetMasterClientCertProvider(nil) })

	cfg, err := tlsConfigForNode(&model.Node{TlsVerifyMode: "mtls"})
	if err != nil {
		t.Fatalf("tlsConfigForNode(mtls): %v", err)
	}
	if len(cfg.Certificates) != 1 {
		t.Fatalf("mtls config must present exactly one client certificate, got %d", len(cfg.Certificates))
	}
	if cfg.InsecureSkipVerify {
		t.Fatal("mtls must NOT skip server verification")
	}
	if cfg.RootCAs != nil {
		t.Fatal("mtls verifies the node server against system roots (RootCAs must be nil)")
	}
}

// TestTLSConfigForNode_MTLS_NoProviderFailsClosed asserts mtls fails closed when
// no master client certificate is available, rather than silently dropping auth.
func TestTLSConfigForNode_MTLS_NoProviderFailsClosed(t *testing.T) {
	SetMasterClientCertProvider(nil)
	if _, err := tlsConfigForNode(&model.Node{TlsVerifyMode: "mtls"}); err == nil {
		t.Fatal("mtls without a configured client cert provider must fail closed")
	}
}

// nodeForServer builds a node pointing at a loopback test server (loopback is
// SSRF-blocked, so AllowPrivateAddress is set for the guarded dialer).
func nodeForServer(t *testing.T, srv *httptest.Server, mode, pin string) *model.Node {
	t.Helper()
	u, err := url.Parse(srv.URL)
	if err != nil {
		t.Fatalf("parse server url: %v", err)
	}
	port, err := strconv.Atoi(u.Port())
	if err != nil {
		t.Fatalf("parse server port: %v", err)
	}
	return &model.Node{
		Id:                  1,
		Name:                "n1",
		Scheme:              "https",
		Address:             u.Hostname(),
		Port:                port,
		BasePath:            "/",
		ApiToken:            "token",
		Enable:              true,
		AllowPrivateAddress: true,
		TlsVerifyMode:       mode,
		PinnedCertSha256:    pin,
	}
}

func leafPinBase64(srv *httptest.Server) string {
	sum := sha256.Sum256(srv.Certificate().Raw)
	return base64.StdEncoding.EncodeToString(sum[:])
}

// A self-signed node must be reachable by Remote ops under skip/pin and
// rejected under verify — the split issue #5264 reported.
func TestRemoteHonorsTLSVerifyMode(t *testing.T) {
	srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"success":true,"obj":[]}`))
	}))
	defer srv.Close()

	goodPin := leafPinBase64(srv)
	wrongPin := base64.StdEncoding.EncodeToString(make([]byte, sha256.Size))

	cases := []struct {
		name    string
		mode    string
		pin     string
		wantErr bool
	}{
		{"verify rejects self-signed", "verify", "", true},
		{"skip accepts self-signed", "skip", "", false},
		{"pin accepts matching cert", "pin", goodPin, false},
		{"pin rejects mismatched cert", "pin", wrongPin, true},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			r := NewRemote(nodeForServer(t, srv, c.mode, c.pin), nil)
			_, err := r.ListInboundOptions(context.Background())
			if c.wantErr && err == nil {
				t.Fatalf("mode %q: expected error, got nil", c.mode)
			}
			if !c.wantErr && err != nil {
				t.Fatalf("mode %q: unexpected error: %v", c.mode, err)
			}
		})
	}
}

// The lazily-built client is cached for the Remote's lifetime so repeated
// operations reuse one pooled transport rather than rebuilding TLS each call.
func TestRemoteClientCached(t *testing.T) {
	r := NewRemote(&model.Node{Scheme: "https", TlsVerifyMode: "skip"}, nil)
	c1, err1 := r.httpClient()
	c2, err2 := r.httpClient()
	if err1 != nil || err2 != nil {
		t.Fatalf("httpClient errors: %v %v", err1, err2)
	}
	if c1 != c2 {
		t.Fatal("expected the same cached client across calls")
	}
}

func TestHTTPClientForNodeVerifyShared(t *testing.T) {
	// verify mode and plain http both reuse the shared default client.
	for _, n := range []*model.Node{
		{Scheme: "https", TlsVerifyMode: "verify"},
		{Scheme: "https", TlsVerifyMode: ""},
		{Scheme: "http", TlsVerifyMode: "skip"},
	} {
		c, err := HTTPClientForNode(n, "")
		if err != nil {
			t.Fatalf("HTTPClientForNode(%+v): %v", n, err)
		}
		if c != defaultNodeHTTPClient {
			t.Fatalf("HTTPClientForNode(%+v) = %p, want shared default %p", n, c, defaultNodeHTTPClient)
		}
	}
}

func TestHTTPClientForNodePinInvalid(t *testing.T) {
	// pin mode must fail closed, and with a specific error per cause — not merely
	// "some error" (which a bug anywhere in the build path would also satisfy).
	cases := []struct {
		name    string
		pin     string
		wantErr string
	}{
		{"garbage pin", "not-a-pin", "must be a SHA-256 hash"},
		{"empty pin", "", "certificate pin is empty"},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			_, err := HTTPClientForNode(&model.Node{Scheme: "https", TlsVerifyMode: "pin", PinnedCertSha256: c.pin}, "")
			if err == nil {
				t.Fatalf("expected error for pin %q", c.pin)
			}
			if !strings.Contains(err.Error(), c.wantErr) {
				t.Fatalf("error = %q, want it to contain %q", err.Error(), c.wantErr)
			}
		})
	}
}

// TestHTTPClientForNode_ProxyPinPreservesPinEnforcement covers the proxy+pin branch
// (tls_client.go:43-52): when a node uses a proxy AND pin mode, the proxy client's
// transport must carry the pinning tls.Config (the `transport.TLSClientConfig = tlsCfg`
// line). Dropping it would silently disable certificate pinning whenever a proxy is set.
func TestHTTPClientForNode_ProxyPinPreservesPinEnforcement(t *testing.T) {
	pin := base64.StdEncoding.EncodeToString(make([]byte, sha256.Size))
	n := &model.Node{Scheme: "https", TlsVerifyMode: "pin", PinnedCertSha256: pin}

	c, err := HTTPClientForNode(n, "socks5://127.0.0.1:1080")
	if err != nil {
		t.Fatalf("HTTPClientForNode: %v", err)
	}
	if c == defaultNodeHTTPClient {
		t.Fatal("proxy client must not be the shared default client")
	}
	tr, ok := c.Transport.(*http.Transport)
	if !ok {
		t.Fatalf("transport is %T, want *http.Transport", c.Transport)
	}
	if tr.TLSClientConfig == nil || tr.TLSClientConfig.VerifyConnection == nil {
		t.Fatal("pin mode over a proxy must install a pinning tls.Config (VerifyConnection); pin enforcement was dropped")
	}
}

// TestHTTPClientForNode_ProxyVerifyNoPin covers the proxy+verify branch
// (tls_client.go:40-42): verify mode over a proxy returns the proxy client as-is,
// using system-CA verification and NOT a pin VerifyConnection.
func TestHTTPClientForNode_ProxyVerifyNoPin(t *testing.T) {
	n := &model.Node{Scheme: "https", TlsVerifyMode: "verify"}
	c, err := HTTPClientForNode(n, "socks5://127.0.0.1:1080")
	if err != nil {
		t.Fatalf("HTTPClientForNode: %v", err)
	}
	if c == defaultNodeHTTPClient {
		t.Fatal("proxy client must not be the shared default client")
	}
	if tr, ok := c.Transport.(*http.Transport); ok && tr.TLSClientConfig != nil && tr.TLSClientConfig.VerifyConnection != nil {
		t.Fatal("verify mode must not install a pin VerifyConnection")
	}
}

// TestTLSConfigForNode_CurrentContract locks the pre-mTLS behavior of
// tlsConfigForNode so the "mtls" branch added later cannot silently regress the
// existing skip/pin modes (characterization — passes on unchanged code).
func TestTLSConfigForNode_CurrentContract(t *testing.T) {
	t.Run("skip disables verification with no VerifyConnection", func(t *testing.T) {
		cfg, err := tlsConfigForNode(&model.Node{TlsVerifyMode: "skip"})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if !cfg.InsecureSkipVerify {
			t.Fatal("skip mode must set InsecureSkipVerify")
		}
		if cfg.VerifyConnection != nil {
			t.Fatal("skip mode must not install a VerifyConnection")
		}
	})
	t.Run("pin installs a VerifyConnection", func(t *testing.T) {
		pin := base64.StdEncoding.EncodeToString(make([]byte, sha256.Size))
		cfg, err := tlsConfigForNode(&model.Node{TlsVerifyMode: "pin", PinnedCertSha256: pin})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if cfg.VerifyConnection == nil {
			t.Fatal("pin mode must install a VerifyConnection")
		}
	})
}

func TestDecodeCertPin(t *testing.T) {
	raw := sha256.Sum256([]byte("cert"))
	hexColon := strings.ToUpper(hex.EncodeToString(raw[:]))
	// reinsert colons in openssl -fingerprint style
	var withColons strings.Builder
	for i := 0; i < len(hexColon); i += 2 {
		if i > 0 {
			withColons.WriteByte(':')
		}
		withColons.WriteString(hexColon[i : i+2])
	}

	cases := []struct {
		name    string
		in      string
		wantErr bool
	}{
		{"base64 std", base64.StdEncoding.EncodeToString(raw[:]), false},
		{"base64 raw url", base64.RawURLEncoding.EncodeToString(raw[:]), false},
		{"hex bare", hex.EncodeToString(raw[:]), false},
		{"hex colon openssl", withColons.String(), false},
		{"empty", "", true},
		{"garbage", "not-a-pin", true},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got, err := DecodeCertPin(c.in)
			if c.wantErr {
				if err == nil {
					t.Fatalf("expected error for %q", c.in)
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error for %q: %v", c.in, err)
			}
			if string(got) != string(raw[:]) {
				t.Fatalf("decoded bytes mismatch for %q", c.in)
			}
		})
	}
}
