package main

import (
	"go/ast"
	"go/parser"
	"go/token"
	"strings"
	"testing"
)

// ---------------------------------------------------------------------------
// Mutation-based self-tests for the per-mutator/callsite closure guardrail.
// Each test mutates one dimension (remove a fence, move a fence after a write,
// add a new uncommitted mutator, misclassify an entry, stale entry, helper that
// authorizes via ordinary membership read) and asserts the exact failure.
// ---------------------------------------------------------------------------

// parseDecl parses a single Go function declaration from source text.
func parseDecl(t *testing.T, src string) *ast.FuncDecl {
	t.Helper()
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, "infer.go", "package p\n"+src, parser.SkipObjectResolution)
	if err != nil {
		t.Fatalf("parse decl: %v\nsrc: %s", err, src)
	}
	for _, decl := range file.Decls {
		if fd, ok := decl.(*ast.FuncDecl); ok {
			return fd
		}
	}
	t.Fatal("no func decl parsed")
	return nil
}

// declChecker builds a checker whose fileFuncs map contains only the given
// function under a synthetic rel path, mirroring the production walk.
func declChecker(fn *ast.FuncDecl, rel string) *mutatorChecker {
	c := newMutatorChecker("../..")
	c.scoped = true
	if c.fileFuncs[rel] == nil {
		c.fileFuncs[rel] = map[string]*ast.FuncDecl{}
	}
	c.fileFuncs[rel][fn.Name.Name] = fn
	return c
}

const fencedBlockSource = `
func (txs TxStore) BlockTaskForAgent(ctx context.Context, agent AgentActor, board BoardRef, taskNumber int64, input TaskBlockInput) (Task, bool, error) {
	channelID, taskID, err := txs.loadAccessibleAgentTaskID(ctx, tx, agent, board, taskNumber)
	if err != nil {
		return Task{}, false, err
	}
	if _, err := txs.lockTaskBoardChannelAuthority(ctx, tx, agent.WorkspaceID, taskID, agent.MemberID); err != nil {
		return Task{}, false, err
	}
	return txs.blockTaskInTx(ctx, tx, agent.WorkspaceID, taskID, agent.MemberID, input)
}
`

// TestMutatorClosureFenceBeforeWritePasses proves a fenced_entry that calls its
// declared fence BEFORE its durable write passes the closure check.
func TestMutatorClosureFenceBeforeWritePasses(t *testing.T) {
	fn := parseDecl(t, fencedBlockSource)
	rel := "internal/store/taskboard/detail_mutation.go"
	c := declChecker(fn, rel)
	c.closure = map[string]mutatorClosureEntry{
		rel + "::" + fn.Name.Name: {File: rel, Function: fn.Name.Name, Classification: mutatorFencedEntry, Fence: "lockTaskBoardChannelAuthority"},
	}
	c.check()
	if c.failed {
		t.Fatalf("expected closure to pass for fence-before-write, got: %v", c.fails)
	}
}

// TestMutatorClosureMissingFenceFails proves removing the declared fence call
// from a fenced_entry fails (the fence is the authorization fact).
func TestMutatorClosureMissingFenceFails(t *testing.T) {
	src := strings.Replace(fencedBlockSource, "lockTaskBoardChannelAuthority(ctx, tx, agent.WorkspaceID, taskID, agent.MemberID)", "loadAuthorizedUserTaskChannelAccess(ctx, tx, userID, agent.WorkspaceID, channelID, taskWriteChannelAction)", 1)
	fn := parseDecl(t, src)
	rel := "internal/store/taskboard/detail_mutation.go"
	c := declChecker(fn, rel)
	c.closure = map[string]mutatorClosureEntry{
		rel + "::" + fn.Name.Name: {File: rel, Function: fn.Name.Name, Classification: mutatorFencedEntry, Fence: "lockTaskBoardChannelAuthority"},
	}
	c.check()
	if !c.failed {
		t.Fatal("expected missing-fence fenced_entry to fail, but it passed")
	}
	found := false
	for _, f := range c.fails {
		if strings.Contains(f, "must call its declared fence") {
			found = true
		}
	}
	if !found {
		t.Fatalf("expected a 'must call its declared fence' failure, got: %v", c.fails)
	}
}

// TestMutatorClosureFenceAfterWriteFails proves a fence call that appears AFTER
// a durable write in the same fenced_entry body fails (fence after side
// effects must be rejected).
func TestMutatorClosureFenceAfterWriteFails(t *testing.T) {
	src := `
func (txs TxStore) UnclaimForAgent(ctx context.Context, agent AgentActor, boardRef BoardRef, taskNumber int64) (Task, error) {
	now := time.Now().UTC()
	if _, err := txs.tx.Exec(ctx, "UPDATE tasks SET assignee_member_id = NULL WHERE id = $1", taskID); err != nil {
		return Task{}, err
	}
	if err := txs.lockActiveBoardChannelMembership(ctx, txs.tx, boardRef.ChannelID, agent.MemberID); err != nil {
		return Task{}, err
	}
	return Task{}, nil
}
`
	fn := parseDecl(t, src)
	rel := "internal/store/taskboard/agent_tasks.go"
	c := declChecker(fn, rel)
	c.closure = map[string]mutatorClosureEntry{
		rel + "::" + fn.Name.Name: {File: rel, Function: fn.Name.Name, Classification: mutatorFencedEntry, Fence: "lockActiveBoardChannelMembership"},
	}
	c.check()
	if !c.failed {
		t.Fatal("expected fence-after-write fenced_entry to fail, but it passed")
	}
	found := false
	for _, f := range c.fails {
		if strings.Contains(f, "BEFORE its durable write") {
			found = true
		}
	}
	if !found {
		t.Fatalf("expected a 'BEFORE its durable write' failure, got: %v", c.fails)
	}
}

// TestMutatorClosureStaleEntryFails proves a committed closure entry whose
// function no longer exists in production fails.
func TestMutatorClosureStaleEntryFails(t *testing.T) {
	c := newMutatorChecker("../..")
	rel := "internal/store/taskboard/detail_mutation.go"
	c.closure = map[string]mutatorClosureEntry{
		rel + "::RemovedMutation": {File: rel, Function: "RemovedMutation", Classification: mutatorFencedEntry, Fence: "lockTaskBoardChannelAuthority"},
	}
	c.check()
	if !c.failed {
		t.Fatal("expected stale closure entry to fail, but it passed")
	}
	found := false
	for _, f := range c.fails {
		if strings.Contains(f, "stale committed mutator closure entry") {
			found = true
		}
	}
	if !found {
		t.Fatalf("expected a 'stale committed mutator closure entry' failure, got: %v", c.fails)
	}
}

// TestMutatorClosureNewUncommittedMutatorFails proves a write-bearing TxStore
// method NOT present in the committed closure fails (a new mutator without a
// fence must never pass). This runs the production-wide scan over the real
// committed closure plus the synthetic direct-write mutator.
func TestMutatorClosureNewUncommittedMutatorFails(t *testing.T) {
	src := `
func (txs TxStore) SneakyDirectWrite(ctx context.Context, workspaceID string, taskID string) (Task, error) {
	now := time.Now().UTC()
	if _, err := txs.tx.Exec(ctx, "UPDATE tasks SET title = 'pwned' WHERE id = $1", taskID); err != nil {
		return Task{}, err
	}
	return txs.taskByID(ctx, txs.tx, workspaceID, taskID)
}
`
	fn := parseDecl(t, src)
	rel := "internal/store/taskboard/agent_tasks.go"
	c := newMutatorChecker("../..")
	c.extraFuncs = map[string]map[string]*ast.FuncDecl{rel: {fn.Name.Name: fn}}
	// committed closure does NOT contain SneakyDirectWrite
	c.check()
	if !c.failed {
		t.Fatal("expected an uncommitted write-bearing mutator to fail, but it passed")
	}
	found := false
	for _, f := range c.fails {
		if strings.Contains(f, "uncommitted Task mutation") {
			found = true
		}
	}
	if !found {
		t.Fatalf("expected an 'uncommitted Task mutation' failure, got: %v", c.fails)
	}
}

// TestMutatorClosureHelperNoMembershipReadPasses proves a fenced_helper that
// writes durable state WITHOUT calling an ordinary membership read passes.
func TestMutatorClosureHelperNoMembershipReadPasses(t *testing.T) {
	src := `
func (txs TxStore) blockTaskInTx(ctx context.Context, tx coredb.Tx, workspaceID string, taskID string, actorMemberID string, input TaskBlockInput) (Task, bool, error) {
	if _, err := tx.Exec(ctx, "UPDATE tasks SET blocked_reason = $2 WHERE id = $1", taskID, input.Reason); err != nil {
		return Task{}, false, err
	}
	return txs.taskByID(ctx, tx, workspaceID, taskID)
}
`
	fn := parseDecl(t, src)
	rel := "internal/store/taskboard/detail_mutation.go"
	c := declChecker(fn, rel)
	c.closure = map[string]mutatorClosureEntry{
		rel + "::" + fn.Name.Name: {File: rel, Function: fn.Name.Name, Classification: mutatorFencedHelper},
	}
	c.check()
	if c.failed {
		t.Fatalf("expected a clean fenced_helper to pass, got: %v", c.fails)
	}
}

// TestMutatorClosureHelperOrdinaryReadFails proves a fenced_helper that calls an
// ordinary membership-read authorizer fails (a sibling relying on an ordinary
// membership read must never be the authority).
func TestMutatorClosureHelperOrdinaryReadFails(t *testing.T) {
	src := `
func (txs TxStore) blockTaskInTx(ctx context.Context, tx coredb.Tx, workspaceID string, taskID string, actorMemberID string, input TaskBlockInput) (Task, bool, error) {
	channelAccess, err := txs.loadAuthorizedUserTaskChannelAccess(ctx, tx, userID, workspaceID, channelID, taskWriteChannelAction)
	if err != nil {
		return Task{}, false, err
	}
	if _, err := tx.Exec(ctx, "UPDATE tasks SET blocked_reason = $2 WHERE id = $1", taskID, input.Reason); err != nil {
		return Task{}, false, err
	}
	return txs.taskByID(ctx, tx, workspaceID, taskID)
}
`
	fn := parseDecl(t, src)
	rel := "internal/store/taskboard/detail_mutation.go"
	c := declChecker(fn, rel)
	c.closure = map[string]mutatorClosureEntry{
		rel + "::" + fn.Name.Name: {File: rel, Function: fn.Name.Name, Classification: mutatorFencedHelper},
	}
	c.check()
	if !c.failed {
		t.Fatal("expected a fenced_helper calling an ordinary membership read to fail, but it passed")
	}
	found := false
	for _, f := range c.fails {
		if strings.Contains(f, "ordinary membership read") {
			found = true
		}
	}
	if !found {
		t.Fatalf("expected an 'ordinary membership read' failure, got: %v", c.fails)
	}
}

// TestMutatorClosureNewScopeMembershipsWriterFails proves a new uncommitted
// TxStore method that writes scope_memberships (a Task-associated durable
// surface beyond tasks/checklist/review/artifact) fails the closure — the fence
// must dominate read-state/notification/activity writes too.
func TestMutatorClosureNewScopeMembershipsWriterFails(t *testing.T) {
	src := `
func (txs TxStore) SneakyReadStateWrite(ctx context.Context, taskScopeID string, memberID string) error {
	now := time.Now().UTC()
	if _, err := txs.tx.Exec(ctx, "UPDATE scope_memberships SET last_read_seq = 99 WHERE scope_id = $1 AND member_id = $2", taskScopeID, memberID); err != nil {
		return err
	}
	return nil
}
`
	fn := parseDecl(t, src)
	rel := "internal/store/taskboard/read_state.go"
	c := newMutatorChecker("../..")
	c.extraFuncs = map[string]map[string]*ast.FuncDecl{rel: {fn.Name.Name: fn}}
	c.check()
	if !c.failed {
		t.Fatal("expected an uncommitted scope_memberships-writing mutator to fail, but it passed")
	}
	found := false
	for _, f := range c.fails {
		if strings.Contains(f, "uncommitted Task mutation") {
			found = true
		}
	}
	if !found {
		t.Fatalf("expected an 'uncommitted Task mutation' failure, got: %v", c.fails)
	}
}

// TestMutatorClosureDuplicateEntryFails proves two committed closure entries at
// the same file::function (a duplicate) are rejected as misclassified.
func TestMutatorClosureDuplicateEntryFails(t *testing.T) {
	// A duplicate key cannot exist in a map, so the production invariant is that
	// committedMutatorClosure() must not produce duplicate file::function keys.
	seen := map[string]string{}
	dup := ""
	for key, entry := range committedMutatorClosure() {
		if prev, ok := seen[key]; ok {
			dup = key + " (" + prev + " vs " + entry.Classification + ")"
			break
		}
		seen[key] = entry.Classification
	}
	if dup != "" {
		t.Fatalf("committed mutator closure has duplicate entry: %s", dup)
	}
}

// TestMutatorClosureProductionClosurePasses proves the real committed closure
// passes against the real production source (the exact same check the script
// runs).
func TestMutatorClosureProductionClosurePasses(t *testing.T) {
	c := newMutatorChecker("../..")
	c.check()
	if c.failed {
		t.Fatalf("production mutator closure guardrail must pass, got: %v", c.fails)
	}
}
