package web

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/robfig/cron/v3"

	"github.com/mhsanaei/3x-ui/v3/internal/database"
	"github.com/mhsanaei/3x-ui/v3/internal/web/global"
)

/*
frontend/src/pages/api-docs/endpoints.ts is a hand-maintained registry: an
API route omitted there silently vanishes from the generated OpenAPI docs,
and an entry for a removed route documents an endpoint that 404s. This test
constructs the real router and diffs it against the registry both ways.

Scope: everything under /panel/api/ plus the session-auth surface the
registry also documents (/login, /logout, /csrf-token, /getTwoFactorEnable,
/ws). SPA page routes are UI, not API, and stay out; registry paths that
start with "/{" describe the standalone subscription server, which this
engine does not serve.
*/

var contractExtraRoutes = map[string]bool{
	"POST /login":              true,
	"POST /logout":             true,
	"GET /csrf-token":          true,
	"POST /getTwoFactorEnable": true,
	"GET /ws":                  true,
}

func inContractScope(method, path string) bool {
	return strings.HasPrefix(path, "/panel/api/") || contractExtraRoutes[method+" "+path]
}

func registeredContractRoutes(t *testing.T) map[string]bool {
	t.Helper()
	if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
		t.Fatalf("init db: %v", err)
	}
	t.Cleanup(func() { _ = database.CloseDB() })

	previous := global.GetWebServer()
	s := NewServer()
	s.cron = cron.New(cron.WithLocation(time.Local), cron.WithSeconds())
	global.SetWebServer(s)
	t.Cleanup(func() {
		s.cancel()
		global.SetWebServer(previous)
	})

	engine, err := s.initRouter()
	if err != nil {
		t.Fatalf("init router: %v", err)
	}
	routes := make(map[string]bool)
	for _, r := range engine.Routes() {
		routes[r.Method+" "+r.Path] = true
	}
	if len(routes) == 0 {
		t.Fatal("no routes registered; router construction is broken")
	}
	return routes
}

func documentedContractRoutes(t *testing.T) map[string]bool {
	t.Helper()
	source, err := os.ReadFile(filepath.Join("..", "..", "frontend", "src", "pages", "api-docs", "endpoints.ts"))
	if err != nil {
		t.Fatalf("read endpoints.ts: %v", err)
	}
	text := string(source)
	methodRe := regexp.MustCompile(`method:\s*'(GET|POST|PUT|DELETE|PATCH|HEAD|WS)'`)
	pathRe := regexp.MustCompile(`path:\s*'([^']+)'`)
	methods := methodRe.FindAllStringSubmatchIndex(text, -1)
	if declared := strings.Count(text, "method: '"); len(methods) != declared {
		t.Fatalf("parsed %d method fields but endpoints.ts declares %d — the parser regex no longer matches the file shape", len(methods), declared)
	}
	docs := make(map[string]bool)
	for i, m := range methods {
		segmentEnd := len(text)
		if i+1 < len(methods) {
			segmentEnd = methods[i+1][0]
		}
		pathMatch := pathRe.FindStringSubmatch(text[m[1]:segmentEnd])
		if pathMatch == nil {
			t.Fatalf("entry %d in endpoints.ts has a method but no path before the next entry — the parser cannot pair it", i)
		}
		method := text[m[2]:m[3]]
		if strings.HasPrefix(pathMatch[1], "/{") || !strings.HasPrefix(pathMatch[1], "/") {
			continue
		}
		docs[method+" "+pathMatch[1]] = true
	}
	if len(docs) == 0 {
		t.Fatal("no entries parsed from endpoints.ts; the parser regex is broken")
	}
	return docs
}

func TestRouteRegistryContract(t *testing.T) {
	registered := registeredContractRoutes(t)
	documented := documentedContractRoutes(t)

	t.Run("every API route is documented", func(t *testing.T) {
		var missing []string
		for route := range registered {
			fields := strings.Fields(route)
			if inContractScope(fields[0], fields[1]) && !documented[route] {
				missing = append(missing, route)
			}
		}
		sort.Strings(missing)
		for _, route := range missing {
			t.Error(fmt.Errorf("route %s is registered but absent from endpoints.ts — add an entry or it vanishes from the API docs", route))
		}
	})

	t.Run("every documented route is registered", func(t *testing.T) {
		var stale []string
		for route := range documented {
			if !registered[route] {
				stale = append(stale, route)
			}
		}
		sort.Strings(stale)
		for _, route := range stale {
			t.Error(fmt.Errorf("endpoints.ts documents %s but the server does not register it — remove or fix the entry", route))
		}
	})
}
