package service

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"encoding/pem"
	"maps"
	"math/big"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"
	"time"

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

	"github.com/xtls/xray-core/infra/conf"
)

// The frontend's golden fixtures are the panel's model of an xray config: the
// Zod snapshots pin what the forms parse and emit. Parsing proves the panel
// agrees with itself, not that xray-core would accept the result, so every
// fixture is also built here through the very config builders the panel hands
// its config to — conf.InboundDetourConfig for the full-config and AddInbound
// paths, conf.RouterConfig for ApplyRoutingConfig, conf.DNSConfig for the dns
// section. A fixture xray-core refuses is a config the panel would let an
// admin save and then fail to start the core with, taking every inbound down.
//
// mtproto is excluded: it is served by the bundled mtg-multi sidecar, not by
// xray, so xray-core has no config id for it.

func goldenFixtureDir(t *testing.T, category string) string {
	t.Helper()
	dir, err := filepath.Abs(filepath.Join("..", "..", "..", "frontend", "src", "test", "golden", "fixtures", category))
	if err != nil {
		t.Fatalf("resolve fixture dir: %v", err)
	}
	return dir
}

// goldenFixtures returns every fixture in a category as name -> decoded object.
func goldenFixtures(t *testing.T, category string) map[string]map[string]any {
	t.Helper()
	dir := goldenFixtureDir(t, category)
	entries, err := os.ReadDir(dir)
	if err != nil {
		t.Fatalf("read %s: %v", dir, err)
	}
	out := make(map[string]map[string]any)
	names := make([]string, 0, len(entries))
	for _, entry := range entries {
		if filepath.Ext(entry.Name()) != ".json" {
			continue
		}
		names = append(names, entry.Name())
	}
	sort.Strings(names)
	if len(names) == 0 {
		t.Fatalf("no fixtures under %s", dir)
	}
	for _, name := range names {
		raw, err := os.ReadFile(filepath.Join(dir, name))
		if err != nil {
			t.Fatalf("read %s: %v", name, err)
		}
		var obj map[string]any
		if err := json.Unmarshal(raw, &obj); err != nil {
			t.Fatalf("unmarshal %s: %v", name, err)
		}
		out[strings.TrimSuffix(name, ".json")] = obj
	}
	return out
}

// writeTestCertificate writes a self-signed certificate and key, returning both
// paths. The TLS fixtures point certificateFile/keyFile at deployment paths
// that do not exist here, and xray-core reads them while building.
func writeTestCertificate(t *testing.T) (certPath, keyPath string) {
	t.Helper()
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	template := x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject:      pkix.Name{CommonName: "golden-fixture.test"},
		NotBefore:    time.Now().Add(-time.Hour),
		NotAfter:     time.Now().Add(24 * time.Hour),
		DNSNames:     []string{"golden-fixture.test"},
	}
	der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
	if err != nil {
		t.Fatalf("create certificate: %v", err)
	}
	keyDER, err := x509.MarshalECPrivateKey(key)
	if err != nil {
		t.Fatalf("marshal key: %v", err)
	}

	dir := t.TempDir()
	certPath = filepath.Join(dir, "fixture.crt")
	keyPath = filepath.Join(dir, "fixture.key")
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
	if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
		t.Fatalf("write certificate: %v", err)
	}
	if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
		t.Fatalf("write key: %v", err)
	}
	return certPath, keyPath
}

// repointCertificateFiles rewrites every certificateFile/keyFile reference to
// the generated pair, so a fixture is judged on its shape rather than on paths
// that only exist on a deployed server.
func repointCertificateFiles(node any, certPath, keyPath string) {
	switch value := node.(type) {
	case map[string]any:
		for key, child := range value {
			switch key {
			case "certificateFile":
				if _, ok := child.(string); ok {
					value[key] = certPath
					continue
				}
			case "keyFile":
				if _, ok := child.(string); ok {
					value[key] = keyPath
					continue
				}
			}
			repointCertificateFiles(child, certPath, keyPath)
		}
	case []any:
		for _, child := range value {
			repointCertificateFiles(child, certPath, keyPath)
		}
	}
}

// assertXrayAccepts fails unless xray-core built the fixture. Fixtures naming
// geoip:/geosite: need the dat files the panel ships next to the xray binary;
// where they are absent the loader fails on the missing file rather than on the
// fixture, so those are skipped instead of reported as broken.
func assertXrayAccepts(t *testing.T, subject string, err error) {
	t.Helper()
	if err == nil {
		return
	}
	if isMissingGeoAssetErr(err) {
		t.Skipf("geo data files not available, cannot judge %s: %v", subject, err)
	}
	t.Fatalf("xray-core refuses %s: %v", subject, err)
}

func buildGoldenInbound(t *testing.T, inbound map[string]any) error {
	t.Helper()
	certPath, keyPath := writeTestCertificate(t)
	repointCertificateFiles(inbound, certPath, keyPath)

	raw, err := json.Marshal(inbound)
	if err != nil {
		t.Fatalf("marshal inbound: %v", err)
	}
	detour := new(conf.InboundDetourConfig)
	if err := json.Unmarshal(raw, detour); err != nil {
		return err
	}
	_, err = detour.Build()
	return err
}

// TestGoldenInboundFixturesBuildInXray wraps each protocol fixture in a minimal
// inbound and builds it.
func TestGoldenInboundFixturesBuildInXray(t *testing.T) {
	for name, fixture := range goldenFixtures(t, "inbound") {
		if protocol, _ := fixture["protocol"].(string); protocol == string(model.MTProto) {
			continue
		}
		t.Run(name, func(t *testing.T) {
			inbound := map[string]any{
				"tag":      "golden-in",
				"listen":   "127.0.0.1",
				"port":     8443,
				"protocol": fixture["protocol"],
				"settings": fixture["settings"],
			}
			assertXrayAccepts(t, "this fixture", buildGoldenInbound(t, inbound))
		})
	}
}

// TestGoldenInboundFullFixturesBuildInXray runs the complete panel Inbound
// model through GenXrayInboundConfig, the conversion both the full-config and
// the live AddInbound paths use, and builds the result.
func TestGoldenInboundFullFixturesBuildInXray(t *testing.T) {
	for name, fixture := range goldenFixtures(t, "inbound-full") {
		protocol, _ := fixture["protocol"].(string)
		if protocol == string(model.MTProto) {
			continue
		}
		t.Run(name, func(t *testing.T) {
			section := func(key string) string {
				value, ok := fixture[key]
				if !ok || value == nil {
					return ""
				}
				raw, err := json.Marshal(value)
				if err != nil {
					t.Fatalf("marshal %s: %v", key, err)
				}
				return string(raw)
			}
			port := 0
			if p, ok := fixture["port"].(float64); ok {
				port = int(p)
			}
			listen, _ := fixture["listen"].(string)
			tag, _ := fixture["tag"].(string)

			ib := &model.Inbound{
				Protocol:       model.Protocol(protocol),
				Port:           port,
				Listen:         listen,
				Tag:            tag,
				Settings:       section("settings"),
				StreamSettings: section("streamSettings"),
				Sniffing:       section("sniffing"),
			}
			raw, err := json.Marshal(ib.GenXrayInboundConfig())
			if err != nil {
				t.Fatalf("marshal generated inbound: %v", err)
			}
			var generated map[string]any
			if err := json.Unmarshal(raw, &generated); err != nil {
				t.Fatalf("decode generated inbound: %v", err)
			}
			assertXrayAccepts(t, "the generated inbound", buildGoldenInbound(t, generated))
		})
	}
}

// TestGoldenStreamFixturesBuildInXray attaches the transport fragments — whole
// stream sections, the security block, sockopt and finalmask — to an inbound.
func TestGoldenStreamFixturesBuildInXray(t *testing.T) {
	for _, category := range []string{"stream", "security", "sockopt", "finalmask"} {
		for name, fixture := range goldenFixtures(t, category) {
			t.Run(category+"/"+name, func(t *testing.T) {
				stream := map[string]any{"network": "tcp"}
				switch category {
				case "stream":
					stream = fixture
				case "security":
					maps.Copy(stream, fixture)
				case "sockopt":
					stream["sockopt"] = fixture
				case "finalmask":
					stream["finalmask"] = fixture
				}
				inbound := map[string]any{
					"tag": "golden-in", "listen": "127.0.0.1", "port": 8443, "protocol": "vless",
					"settings": map[string]any{
						"clients":    []any{map[string]any{"id": "b831381d-6324-4d53-ad4f-8cda48b30811", "email": "golden"}},
						"decryption": "none",
					},
					"streamSettings": stream,
				}
				assertXrayAccepts(t, "this fixture", buildGoldenInbound(t, inbound))
			})
		}
	}
}

func buildGoldenRouting(routing map[string]any) error {
	raw, err := json.Marshal(routing)
	if err != nil {
		return err
	}
	router := new(conf.RouterConfig)
	if err := json.Unmarshal(raw, router); err != nil {
		return err
	}
	_, err = router.Build()
	return err
}

// TestGoldenRoutingFixturesBuildInXray builds the rule and balancer fixtures
// through the router config ApplyRoutingConfig hands to the running core.
func TestGoldenRoutingFixturesBuildInXray(t *testing.T) {
	for name, rule := range goldenFixtures(t, "rule") {
		t.Run("rule/"+name, func(t *testing.T) {
			assertXrayAccepts(t, "this rule", buildGoldenRouting(map[string]any{
				"domainStrategy": "AsIs",
				"rules":          []any{rule},
				"balancers":      []any{map[string]any{"tag": "balancer-load", "selector": []any{"proxy-"}}},
			}))
		})
	}

	for name, balancer := range goldenFixtures(t, "balancer") {
		t.Run("balancer/"+name, func(t *testing.T) {
			assertXrayAccepts(t, "this balancer", buildGoldenRouting(map[string]any{
				"domainStrategy": "AsIs",
				"balancers":      []any{balancer},
				"rules": []any{map[string]any{
					"type": "field", "port": "443", "balancerTag": balancer["tag"],
				}},
			}))
		})
	}
}

// TestGoldenDNSFixturesBuildInXray builds the dns section, and each dns-server
// fixture inside one.
func TestGoldenDNSFixturesBuildInXray(t *testing.T) {
	build := func(dns map[string]any) error {
		raw, err := json.Marshal(dns)
		if err != nil {
			return err
		}
		dnsConf := new(conf.DNSConfig)
		if err := json.Unmarshal(raw, dnsConf); err != nil {
			return err
		}
		_, err = dnsConf.Build()
		return err
	}

	for name, fixture := range goldenFixtures(t, "dns") {
		t.Run("dns/"+name, func(t *testing.T) {
			assertXrayAccepts(t, "this dns section", build(fixture))
		})
	}
	for name, server := range goldenFixtures(t, "dns-server") {
		t.Run("dns-server/"+name, func(t *testing.T) {
			assertXrayAccepts(t, "this dns server", build(map[string]any{"servers": []any{server}}))
		})
	}
}

func isMissingGeoAssetErr(err error) bool {
	msg := err.Error()
	return strings.Contains(msg, "geoip.dat") || strings.Contains(msg, "geosite.dat")
}
