package auth

import (
	"go/ast"
	"go/parser"
	"go/token"
	"path/filepath"
	"sort"
	"strconv"
	"testing"
)

// TestPermissionCatalogCoversEnforcedKeys is the anti-drift net: every
// permission literal enforced via RequirePermission in cmd/server/main.go
// and internal/*/handler.go must exist in the catalog, otherwise key
// validation would reject live grants or editors could not show live gates.
func TestPermissionCatalogCoversEnforcedKeys(t *testing.T) {
	literals, nonLiterals := enforcedPermissionLiterals(t)
	if len(nonLiterals) > 0 {
		t.Logf("non-literal RequirePermission calls (not validated): %v", nonLiterals)
	}
	if len(literals) == 0 {
		t.Fatal("no RequirePermission literals found; parser is broken")
	}
	sort.Strings(literals)
	for _, k := range literals {
		if !IsKnownPermission(k) {
			t.Errorf("enforced permission %q missing from catalog", k)
		}
	}
}

func TestPermissionCatalogWellFormed(t *testing.T) {
	groups := Catalog().Groups
	if len(groups) == 0 {
		t.Fatal("catalog must hold at least one group")
	}
	seen := map[string]string{}
	for _, g := range groups {
		if g.Label == "" {
			t.Error("catalog group with empty label")
		}
		if len(g.Keys) == 0 {
			t.Errorf("catalog group %q holds no keys", g.Label)
		}
		for _, k := range g.Keys {
			if k.Key == "" || k.Label == "" {
				t.Errorf("catalog group %q holds an empty key/label entry", g.Label)
			}
			if prev, ok := seen[k.Key]; ok {
				t.Errorf("catalog key %q listed in both %q and %q", k.Key, prev, g.Label)
			}
			seen[k.Key] = g.Label
		}
	}
}

// enforcedPermissionLiterals parses every RequirePermission("literal") call
// in cmd/server/main.go and internal/*/handler.go. Calls with a non-literal
// argument are returned separately (logged, never failed).
func enforcedPermissionLiterals(t *testing.T) (literals, nonLiterals []string) {
	t.Helper()
	fset := token.NewFileSet()
	seenLit := map[string]bool{}
	collect := func(path string) {
		file, err := parser.ParseFile(fset, path, nil, 0)
		if err != nil {
			t.Fatalf("parse %s: %v", path, err)
		}
		ast.Inspect(file, func(node ast.Node) bool {
			call, ok := node.(*ast.CallExpr)
			if !ok {
				return true
			}
			sel, ok := call.Fun.(*ast.SelectorExpr)
			if !ok || (sel.Sel.Name != "RequirePermission" && sel.Sel.Name != "RequireAnyPermission") || len(call.Args) == 0 {
				return true
			}
			for _, arg := range call.Args {
				lit, ok := arg.(*ast.BasicLit)
				if !ok {
					nonLiterals = append(nonLiterals, path)
					continue
				}
				s, err := strconv.Unquote(lit.Value)
				if err != nil {
					t.Fatalf("unquote %s in %s: %v", lit.Value, path, err)
				}
				if !seenLit[s] {
					seenLit[s] = true
					literals = append(literals, s)
				}
			}
			return true
		})
	}
	collect(filepath.Join("..", "..", "cmd", "server", "main.go"))
	matches, err := filepath.Glob(filepath.Join("..", "*", "handler.go"))
	if err != nil {
		t.Fatal(err)
	}
	for _, m := range matches {
		collect(m)
	}
	return literals, nonLiterals
}
