package main

import (
	"strings"
	"testing"
)

// ---------------------------------------------------------------------------
// Mutation-based self-tests for the fail-closed three-way bijection.
//
// Each test constructs the actual occurrence set, the committed inventory, and
// the reviewed policy, then invokes checkWithSets (the testable core that the
// production check() delegates to) and asserts the exact failure mode — or a
// clean pass for the exact closure. The sets are synthetic so a mutation
// (remove/add/misclassify/stale) deterministically exercises one invariant.
// ---------------------------------------------------------------------------

const (
	sigFile = "internal/store/access/authorizer.go"
	sigFn   = "AuthorizeActiveAgentChannelMembership"
	sigLine = 346
)

// occ builds one synthetic actual occurrence with a stable per-callsite
// signature.
func occ(d string) occurrence {
	sig := occurrenceSignature(sigFile, sigFn, sigLine, d)
	return occurrence{rel: sigFile, fn: sigFn, line: sigLine, digest: d, signature: sig}
}

// inv builds the committed inventory record for one signature.
func inv(sig, classification string) inventoryEntry {
	parts := strings.Split(sig, "::")
	return inventoryEntry{
		Classification: classification,
		File:           parts[0],
		Function:       parts[1],
		Line:           lineFromSignature(parts[2]),
	}
}

func lineFromSignature(raw string) int {
	var line int
	for _, ch := range raw {
		if ch < '0' || ch > '9' {
			break
		}
		line = line*10 + int(ch-'0')
	}
	return line
}

func assertFailsWith(t *testing.T, c *checker, substrings ...string) {
	t.Helper()
	if !c.failed {
		t.Fatalf("expected checker to fail for %v, but it passed", substrings)
	}
	for _, sub := range substrings {
		found := false
		for _, fail := range c.fails {
			if strings.Contains(fail, sub) {
				found = true
				break
			}
		}
		if !found {
			t.Fatalf("expected a failure containing %q; got failures: %v", sub, c.fails)
		}
	}
}

func assertPasses(t *testing.T, c *checker) {
	t.Helper()
	if c.failed {
		t.Fatalf("expected checker to pass, but it failed: %v", c.fails)
	}
}

// TestBijectionExactClosurePasses is the passing exact-closure case: every
// actual occurrence has exactly one committed inventory record and exactly one
// independent policy decision, and both reverse directions hold.
func TestBijectionExactClosurePasses(t *testing.T) {
	d1 := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1 AND sm.member_id = $2")
	d2 := digest("SELECT 1 FROM scope_memberships sm JOIN tasks t ON t.task_scope_id = sm.scope_id WHERE sm.scope_id = $1")
	actual := []occurrence{occ(d1), occ(d2)}
	inventory := map[string]inventoryEntry{
		occ(d1).signature: inv(occ(d1).signature, ClassificationAuthoritativePath),
		occ(d2).signature: inv(occ(d2).signature, ClassificationTaskStateOnly),
	}
	policy := map[string]string{
		occ(d1).signature: ClassificationAuthoritativePath,
		occ(d2).signature: ClassificationTaskStateOnly,
	}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertPasses(t, c)
}

// TestBijectionMissingPolicyFails proves an actual occurrence with a committed
// inventory record but NO independent policy decision fails closed (never accept
// an inventory occurrence merely because policy lookup is absent).
func TestBijectionMissingPolicyFails(t *testing.T) {
	d := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	actual := []occurrence{occ(d)}
	inventory := map[string]inventoryEntry{
		occ(d).signature: inv(occ(d).signature, ClassificationAuthoritativePath),
	}
	policy := map[string]string{} // missing policy decision
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "no independent policy decision")
}

// TestBijectionMissingInventoryFails proves an actual occurrence with NO
// committed inventory record fails.
func TestBijectionMissingInventoryFails(t *testing.T) {
	d := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	actual := []occurrence{occ(d)}
	inventory := map[string]inventoryEntry{} // missing inventory
	policy := map[string]string{occ(d).signature: ClassificationAuthoritativePath}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "no committed inventory record")
}

// TestBijectionStalePolicyFails proves a policy decision that corresponds to no
// actual occurrence (stale policy) fails.
func TestBijectionStalePolicyFails(t *testing.T) {
	d := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	actual := []occurrence{}
	inventory := map[string]inventoryEntry{}
	policy := map[string]string{occ(d).signature: ClassificationAuthoritativePath}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "stale policy decision")
}

// TestBijectionStaleInventoryFails proves a committed inventory record that
// corresponds to no actual occurrence (stale inventory) fails.
func TestBijectionStaleInventoryFails(t *testing.T) {
	d := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	actual := []occurrence{}
	inventory := map[string]inventoryEntry{
		occ(d).signature: inv(occ(d).signature, ClassificationAuthoritativePath),
	}
	policy := map[string]string{}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "stale inventory record")
}

// TestBijectionDigestMismatchFails proves an actual occurrence whose SQL digest
// differs from the committed record at the same file::function::line fails: a
// digest change produces a NEW per-occurrence signature that is not committed,
// so the actual occurrence has no committed inventory record (the security fact
// changed and must be re-reviewed).
func TestBijectionDigestMismatchFails(t *testing.T) {
	d1 := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	d2 := digest("SELECT 1 FROM scope_memberships sm JOIN tasks t ON t.task_scope_id = sm.scope_id WHERE sm.scope_id = $1")
	actual := []occurrence{occ(d1)}
	// The committed inventory record exists for the OTHER digest at the same
	// file::function::line (a prior reviewed state). The actual occurrence's
	// signature (d1) is not committed, so it fails.
	inventory := map[string]inventoryEntry{
		occ(d2).signature: inv(occ(d2).signature, ClassificationAuthoritativePath),
	}
	policy := map[string]string{occ(d2).signature: ClassificationAuthoritativePath}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "no committed inventory record")
}

// TestBijectionDuplicateOccurrenceFails proves two actual occurrences with the
// SAME per-callsite signature (a genuine duplicate in the actual set) fail
// closed.
func TestBijectionDuplicateOccurrenceFails(t *testing.T) {
	d := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	same := occ(d)
	actual := []occurrence{occ(d), same} // duplicate signature
	inventory := map[string]inventoryEntry{
		occ(d).signature: inv(occ(d).signature, ClassificationAuthoritativePath),
	}
	policy := map[string]string{
		occ(d).signature: ClassificationAuthoritativePath,
	}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "duplicate actual occurrence")
}

// TestBijectionDifferentlyClassifiedSiblingCallsitesFails proves two actual
// callsites in the SAME function with DIFFERENT reviewed classifications are
// representable and independently checked. A sibling whose policy/inventory
// classification disagrees fails.
func TestBijectionDifferentlyClassifiedSiblingCallsitesFails(t *testing.T) {
	d1 := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1 AND sm.member_id = $2")
	d2 := digest("SELECT 1 FROM scope_memberships sm JOIN tasks t ON t.task_scope_id = sm.scope_id WHERE sm.scope_id = $1")
	// Same function, two lines (sibling callsites).
	o1 := occ(d1)
	o2 := occ(d2)
	o2.line = sigLine + 5
	o2.signature = occurrenceSignature(o2.rel, o2.fn, o2.line, o2.digest)

	actual := []occurrence{o1, o2}
	inventory := map[string]inventoryEntry{
		o1.signature: inv(o1.signature, ClassificationAuthoritativePath),
		o2.signature: inv(o2.signature, ClassificationTaskStateOnly),
	}
	// Policy disagrees with inventory for the sibling (canonical vs task_state).
	policy := map[string]string{
		o1.signature: ClassificationAuthoritativePath,
		o2.signature: ClassificationAuthoritativePath,
	}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "does not match reviewed policy")
}

// TestBijectionClassificationMismatchFails proves an inventory record whose
// classification differs from its independent policy decision fails.
func TestBijectionClassificationMismatchFails(t *testing.T) {
	d := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	actual := []occurrence{occ(d)}
	inventory := map[string]inventoryEntry{
		occ(d).signature: inv(occ(d).signature, ClassificationTaskStateOnly),
	}
	policy := map[string]string{occ(d).signature: ClassificationAuthoritativePath}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "does not match reviewed policy")
}

// TestBijectionPolicyWithoutInventoryFails proves a policy decision that has a
// matching actual occurrence but no committed inventory record fails.
func TestBijectionPolicyWithoutInventoryFails(t *testing.T) {
	d := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	actual := []occurrence{occ(d)}
	inventory := map[string]inventoryEntry{}
	policy := map[string]string{occ(d).signature: ClassificationAuthoritativePath}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "no committed inventory record")
}

// TestBijectionInventoryWithoutPolicyFails proves a committed inventory record
// whose actual occurrence is present but whose policy decision is missing fails.
func TestBijectionInventoryWithoutPolicyFails(t *testing.T) {
	d := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	actual := []occurrence{occ(d)}
	inventory := map[string]inventoryEntry{
		occ(d).signature: inv(occ(d).signature, ClassificationAuthoritativePath),
	}
	policy := map[string]string{}
	c := &checker{}
	c.checkWithSets(actual, inventory, policy)
	assertFailsWith(t, c, "no independent policy decision")
}

// ---------------------------------------------------------------------------
// Digest / normalization self-tests (unchanged security invariants).
// ---------------------------------------------------------------------------

// TestInventoryDigestNormalizesAliases proves the structural digest is invariant
// under SQL alias renames, so a bypass that renames `sm` to `msm` (or any other
// identifier) still maps to the same committed digest and cannot escape the
// inventory by aliasing alone.
func TestInventoryDigestNormalizesAliases(t *testing.T) {
	original := "SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1 AND sm.member_id = $2"
	renamed := "SELECT 1 FROM scope_memberships msm WHERE msm.scope_id = $1 AND msm.member_id = $2"

	if digest(original) != digest(renamed) {
		t.Fatal("alias rename must produce the same structural digest")
	}
}

// TestInventoryDigestRetainsSecurityTopology proves the digest preserves the
// security fact: a Channel-scoped membership query and a direct Task-scope
// membership query MUST produce different digests, so changing an allowlisted
// Channel query to a direct Task membership in the same file/function is caught.
func TestInventoryDigestRetainsSecurityTopology(t *testing.T) {
	channel := "SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1 AND sm.member_id = $2"
	directTask := "SELECT 1 FROM scope_memberships sm JOIN tasks t ON t.task_scope_id = sm.scope_id WHERE sm.scope_id = $1"

	if digest(channel) == digest(directTask) {
		t.Fatal("a Channel-scoped membership query and a direct Task-scope membership query MUST have different digests")
	}
}

// TestInventoryDigestDistinguishesScopeFirstFromJoin proves a scope-first query
// and a join-first query produce different digests.
func TestInventoryDigestDistinguishesScopeFirstFromJoin(t *testing.T) {
	scopeFirst := "SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1"
	joinFirst := "SELECT 1 FROM tasks t JOIN scope_memberships sm ON sm.scope_id = t.task_scope_id WHERE sm.scope_id = $1"

	if digest(scopeFirst) == digest(joinFirst) {
		t.Fatal("scope-first and join-first queries MUST have different digests")
	}
}

// TestInventoryDigestRejectsUnknownScopeMembershipsQuery proves an unclassified
// scope_memberships query produces a digest that is not committed, so a new
// direct-membership gate is caught.
func TestInventoryDigestRejectsUnknownScopeMembershipsQuery(t *testing.T) {
	unknown := "SELECT 1 FROM scope_memberships sm WHERE sm.visibility = 'public' AND sm.scope_id = $1"
	c := newChecker("../..")
	if _, committed := c.inventory[occurrenceSignature(sigFile, sigFn, 1, digest(unknown))]; committed {
		t.Fatal("an unknown scope_memberships query must not be committed")
	}
}

// TestInventoryDigestFoldConcatProvesConcatenatedSQLIsCaught proves
// concatenated SQL fragments fold to the same digest as the joined literal, so
// a bypass that splits the SQL string is caught.
func TestInventoryDigestFoldConcatProvesConcatenatedSQLIsCaught(t *testing.T) {
	joined := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = $1")
	concatA := digest("SELECT 1 FROM scope_memberships sm WHERE sm.scope" + "_id = $1")
	concatB := digest("SELECT 1 FROM scope_memberships sm WHERE " + "sm.scope_id = $1")

	if joined != concatA || joined != concatB {
		t.Fatal("concatenated SQL fragments must fold to the same structural digest")
	}
}

// TestInventoryClassificationMetadataIsCarried proves the committed inventory
// entry carries classification and exact callsite metadata.
func TestInventoryClassificationMetadataIsCarried(t *testing.T) {
	c := newChecker("../..")
	for key, entry := range c.inventory {
		parts := strings.Split(key, "::")
		if len(parts) != 4 {
			t.Fatalf("inventory key %q must be file::function::line::digest", key)
		}
		if entry.Classification == "" {
			t.Fatalf("inventory entry %s must carry a classification", key)
		}
		if entry.File == "" || entry.Function == "" || entry.Line <= 0 {
			t.Fatalf("inventory entry %s must carry exact file/function/line metadata", key)
		}
	}
}

// TestInventoryEveryCommittedOccurrenceHasPolicyAndActual proves the production
// committed inventory + policy + actual occurrence sets form the exact closure:
// every committed inventory record has a policy decision AND an actual
// occurrence, and every policy decision has a committed inventory record. This
// is the real-script three-way bijection (checkWithSets with the real sets).
func TestInventoryEveryCommittedOccurrenceHasPolicyAndActual(t *testing.T) {
	c := newChecker("../..")
	actual := c.collectActual()
	actualBySig := make(map[string]bool, len(actual))
	for _, occ := range actual {
		actualBySig[occ.signature] = true
	}
	for sig, entry := range c.inventory {
		if _, ok := c.policy[sig]; !ok {
			t.Fatalf("committed inventory occurrence %s has no policy decision", sig)
		}
		if !actualBySig[sig] {
			t.Fatalf("committed inventory occurrence %s has no actual occurrence", sig)
		}
		if c.policy[sig] != entry.Classification {
			t.Fatalf("policy classification %q for %s does not match inventory %q", c.policy[sig], sig, entry.Classification)
		}
	}
	for sig := range c.policy {
		if _, ok := c.inventory[sig]; !ok {
			t.Fatalf("policy decision %s has no committed inventory record", sig)
		}
		if !actualBySig[sig] {
			t.Fatalf("policy decision %s has no actual occurrence", sig)
		}
	}
}
