package capability

import (
	"os"
	"reflect"
	"regexp"
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-agent/internal/escrow"
)

// sudoersPath is the in-repo allowlist, relative to this test file (internal/capability/).
const sudoersPath = "../../configs/felhom-agent.sudoers"

// parseSudoersEntries returns every command pattern from the Cmnd_Alias blocks, with the sudoers
// escapes (`\,` `\:`) unescaped. It joins continuation lines and splits the alias RHS on commas
// that are NOT backslash-escaped (escaped commas are literal arg chars, e.g. the lvs `-o` list).
func parseSudoersEntries(t *testing.T, text string) []string {
	t.Helper()
	// 1. Collapse line continuations, keeping only Cmnd_Alias RHS text.
	var rhs strings.Builder
	lines := strings.Split(text, "\n")
	inAlias := false
	for _, ln := range lines {
		trimmed := strings.TrimSpace(ln)
		if strings.HasPrefix(trimmed, "#") {
			continue
		}
		if strings.HasPrefix(trimmed, "Cmnd_Alias ") {
			inAlias = true
			if eq := strings.IndexByte(trimmed, '='); eq >= 0 {
				trimmed = trimmed[eq+1:]
			}
		} else if !inAlias {
			continue
		}
		// The final NOPASSWD line ("felhom-agent ALL=...") ends the alias region.
		if strings.Contains(trimmed, "ALL=(") {
			inAlias = false
			continue
		}
		cont := strings.HasSuffix(trimmed, "\\")
		rhs.WriteString(strings.TrimSuffix(trimmed, "\\"))
		rhs.WriteString(" ")
		if !cont {
			// A non-continued line is the last entry of this alias. Emit a comma so it does not
			// merge with the next alias's first entry when all RHS text is concatenated.
			rhs.WriteString(", ")
			inAlias = false
		}
	}
	// 2. Split on unescaped commas → individual command entries.
	raw := rhs.String()
	var entries []string
	var cur strings.Builder
	for i := 0; i < len(raw); i++ {
		c := raw[i]
		if c == '\\' && i+1 < len(raw) {
			cur.WriteByte(raw[i+1]) // unescape: keep the next char literally (\, → , ; \: → :)
			i++
			continue
		}
		if c == ',' {
			entries = appendTrimmed(entries, cur.String())
			cur.Reset()
			continue
		}
		cur.WriteByte(c)
	}
	entries = appendTrimmed(entries, cur.String())
	return entries
}

func appendTrimmed(entries []string, s string) []string {
	if t := strings.Join(strings.Fields(s), " "); t != "" {
		return append(entries, t)
	}
	return entries
}

// globToRegex translates a sudoers fnmatch pattern to an anchored regex. It is NOT a perfect sudo
// emulator — it only needs to catch a removed/renamed grant (the real failure mode). `*` → `.*`,
// `[...]` char classes pass through (valid regex), regex metachars are escaped.
func globToRegex(pat string) *regexp.Regexp {
	var b strings.Builder
	b.WriteString("^")
	for i := 0; i < len(pat); i++ {
		c := pat[i]
		switch {
		case c == '*':
			b.WriteString(".*")
		case c == '[': // copy the char class verbatim (valid in regex too)
			if j := strings.IndexByte(pat[i:], ']'); j > 0 {
				b.WriteString(pat[i : i+j+1])
				i += j
				continue
			}
			b.WriteString("\\[")
		case strings.IndexByte(`.+()|{}^$\?`, c) >= 0:
			b.WriteByte('\\')
			b.WriteByte(c)
		default:
			b.WriteByte(c)
		}
	}
	b.WriteString("$")
	return regexp.MustCompile(b.String())
}

// matchesAny reports whether cmdline matches at least one sudoers entry pattern.
func matchesAny(cmdline string, entries []string) bool {
	for _, e := range entries {
		if globToRegex(e).MatchString(cmdline) {
			return true
		}
	}
	return false
}

// TestManifestCoveredBySudoers is the headline build-time gate: EVERY manifest capability's
// representative command line must be permitted by at least one sudoers pattern. This is exactly
// the check that would have caught the lxc-info / make-private grants being dropped at the
// 2026-06-28 cutover — in CI, before shipping.
func TestManifestCoveredBySudoers(t *testing.T) {
	data, err := os.ReadFile(sudoersPath)
	if err != nil {
		t.Fatalf("read sudoers %s: %v", sudoersPath, err)
	}
	entries := parseSudoersEntries(t, string(data))
	if len(entries) < 20 {
		t.Fatalf("parsed only %d sudoers entries — parser likely broke", len(entries))
	}
	for _, c := range Manifest() {
		cmdline := strings.TrimSpace(c.Binary + " " + strings.Join(c.ReprArgs, " "))
		if !matchesAny(cmdline, entries) {
			t.Errorf("capability %q (%s) NOT covered by any sudoers grant:\n  %s",
				c.Name, c.Feature, cmdline)
		}
	}
}

// TestRedProof_DroppedGrantFailsCheck is the companion red-proof: with the lxc-info line removed
// from an in-memory copy of the sudoers, the coverage check for guest-init-pid MUST fail. Proves
// the build gate actually catches the regression (a green test that can never go red is hollow).
func TestRedProof_DroppedGrantFailsCheck(t *testing.T) {
	data, err := os.ReadFile(sudoersPath)
	if err != nil {
		t.Fatalf("read sudoers: %v", err)
	}
	// Drop the lxc-info grant line.
	var kept []string
	for _, ln := range strings.Split(string(data), "\n") {
		if strings.Contains(ln, "lxc-info") {
			continue
		}
		kept = append(kept, ln)
	}
	mutated := strings.Join(kept, "\n")
	if strings.Contains(mutated, "lxc-info") {
		t.Fatal("setup: lxc-info line not removed")
	}
	entries := parseSudoersEntries(t, mutated)

	var guestInit Capability
	for _, c := range Manifest() {
		if c.Name == "guest-init-pid" {
			guestInit = c
		}
	}
	if guestInit.Name == "" {
		t.Fatal("manifest missing guest-init-pid")
	}
	cmdline := guestInit.Binary + " " + strings.Join(guestInit.ReprArgs, " ")
	if matchesAny(cmdline, entries) {
		t.Errorf("red-proof FAILED: guest-init-pid still matches after dropping the lxc-info grant — the build gate would NOT catch the regression")
	}

	// Sanity: the UNMUTATED file MUST cover it (so the failure above is specific to the drop).
	full := parseSudoersEntries(t, string(data))
	if !matchesAny(cmdline, full) {
		t.Errorf("guest-init-pid should be covered by the real sudoers")
	}
}

// TestRedProof_DroppedControllerSwapTeeFailsCheck is the companion red-proof for the v0.45.0
// FELHOM_CONTROLLERSWAP grants: with the `tee /etc/felhom-controller-image` line removed, the
// controllerswap-write capability MUST be reported uncovered. Proves the build gate watches the new
// swap write grant (so dropping it can't ship a non-root agent that silently can't auto-update).
func TestRedProof_DroppedControllerSwapTeeFailsCheck(t *testing.T) {
	data, err := os.ReadFile(sudoersPath)
	if err != nil {
		t.Fatalf("read sudoers: %v", err)
	}
	var kept []string
	for _, ln := range strings.Split(string(data), "\n") {
		if strings.Contains(ln, "tee /etc/felhom-controller-image") {
			continue
		}
		kept = append(kept, ln)
	}
	mutated := strings.Join(kept, "\n")
	entries := parseSudoersEntries(t, mutated)

	var write Capability
	for _, c := range Manifest() {
		if c.Name == "controllerswap-write" {
			write = c
		}
	}
	if write.Name == "" {
		t.Fatal("manifest missing controllerswap-write")
	}
	cmdline := write.Binary + " " + strings.Join(write.ReprArgs, " ")
	if matchesAny(cmdline, entries) {
		t.Errorf("red-proof FAILED: controllerswap-write still matches after dropping the tee grant")
	}
	if full := parseSudoersEntries(t, string(data)); !matchesAny(cmdline, full) {
		t.Errorf("controllerswap-write should be covered by the real sudoers")
	}
}

// TestEscrowCeremonyArgvPinned locks the ceremony argv copies together (Scenario G, v0.88.0).
// The exec runner and the manifest entry both consume escrow.CeremonyArgs() (one shared source),
// and TestManifestCoveredBySudoers proves manifest ⊆ sudoers — so pinning the shared source to
// the EXPECTED literal here transitively locks all three: runner == manifest == sudoers.
// Red-proof: mutate one element of the argv in internal/escrow/ceremony.go and THIS test fails
// (and so does the sudoers coverage); a sudoers-side mutation is caught by the existing
// TestRedProof_* machinery.
func TestEscrowCeremonyArgvPinned(t *testing.T) {
	wantBinary := "/usr/local/bin/felhom-agent"
	wantArgs := []string{"--config", "/etc/felhom-agent/agent.json", "--selftest=escrow-create", "--upload", "--output=json"}

	if escrow.CeremonyBinary != wantBinary {
		t.Errorf("escrow.CeremonyBinary = %q, want %q", escrow.CeremonyBinary, wantBinary)
	}
	if got := escrow.CeremonyArgs(); !reflect.DeepEqual(got, wantArgs) {
		t.Errorf("escrow.CeremonyArgs() = %q, want %q (the sudoers line + manifest entry must stay byte-identical)", got, wantArgs)
	}

	var entry Capability
	for _, c := range Manifest() {
		if c.Name == "escrow-ceremony" {
			entry = c
		}
	}
	if entry.Name == "" {
		t.Fatal("manifest missing escrow-ceremony")
	}
	if entry.Binary != escrow.CeremonyBinary || !reflect.DeepEqual(entry.ReprArgs, escrow.CeremonyArgs()) {
		t.Errorf("manifest escrow-ceremony argv diverged from the shared constant: %s %q", entry.Binary, entry.ReprArgs)
	}
	if !entry.Critical {
		t.Error("escrow-ceremony must be Critical (the wizard's whole run path is this one grant)")
	}
	if entry.GatedBy != GatePBSDR {
		t.Errorf("escrow-ceremony GatedBy = %q, want %q (no PBS key → no ceremony; inactive, never red, on a DR-off box)", entry.GatedBy, GatePBSDR)
	}
	// CeremonyArgs must return a COPY — a caller mutating its slice must not poison the source.
	mutated := escrow.CeremonyArgs()
	mutated[0] = "--poisoned"
	if got := escrow.CeremonyArgs(); !reflect.DeepEqual(got, wantArgs) {
		t.Error("escrow.CeremonyArgs() shares its backing array — callers can mutate the source")
	}
}

// TestWGCapabilityCriticality pins the exact S4 (v0.66.0) Critical set for the FELHOM_WG entries:
// the backup path (conf install, unit enable/restart, handshake read) is operator-alert-worthy now
// that offsite backups ride the tunnel; the one-time apt install and the deliberate disable
// (revocation) are NOT. Red-proof: flip any one entry's Critical in manifest.go and this fails.
func TestWGCapabilityCriticality(t *testing.T) {
	wantCritical := map[string]bool{
		"wg-tools-install":  false,
		"wg-conf-install":   true,
		"wg-enable":         true,
		"wg-restart":        true,
		"wg-disable":        false,
		"wg-handshake-read": true,
	}
	seen := map[string]bool{}
	for _, c := range Manifest() {
		want, ok := wantCritical[c.Name]
		if !ok {
			continue
		}
		seen[c.Name] = true
		if c.Critical != want {
			t.Errorf("%s: Critical = %v, want %v", c.Name, c.Critical, want)
		}
	}
	for name := range wantCritical {
		if !seen[name] {
			t.Errorf("manifest missing wg capability %q", name)
		}
	}
}
