package agents

import (
	"bytes"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestAppendInstructions_CreateThenSkip pins the behaviour every
// doc-aware adapter depends on: first call creates the file, second
// call with the same body is a no-op ActionSkip. If this regresses,
// running `gortex init` twice would append the block twice to every
// agent's rules file — the user-visible pain we extracted this helper
// to eliminate.
func TestAppendInstructions_CreateThenSkip(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "nested", "rules.md")

	var buf bytes.Buffer
	action, err := AppendInstructions(&buf, path, InstructionsBody, InstructionsSentinel, ApplyOpts{})
	require.NoError(t, err)
	assert.Equal(t, ActionCreate, action.Action)

	contents, err := os.ReadFile(path)
	require.NoError(t, err)
	assert.Contains(t, string(contents), InstructionsSentinel,
		"first write must land the full sentinel-bearing block")

	// Second call is idempotent — no duplicate append.
	action, err = AppendInstructions(&buf, path, InstructionsBody, InstructionsSentinel, ApplyOpts{})
	require.NoError(t, err)
	assert.Equal(t, ActionSkip, action.Action)
	assert.Equal(t, "block-present", action.Reason)

	after, err := os.ReadFile(path)
	require.NoError(t, err)
	assert.Equal(t, len(contents), len(after),
		"second call must not grow the file")
}

// TestAppendInstructions_PreservesExistingContent guards the merge
// path — the helper must not clobber a hand-written file, it must
// append the block after the user's content with a blank-line gap.
func TestAppendInstructions_PreservesExistingContent(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "AGENTS.md")
	existing := "# Team conventions\n\nUse tabs, not spaces.\n"
	require.NoError(t, os.WriteFile(path, []byte(existing), 0o644))

	action, err := AppendInstructions(nil, path, InstructionsBody, InstructionsSentinel, ApplyOpts{})
	require.NoError(t, err)
	assert.Equal(t, ActionMerge, action.Action)

	data, err := os.ReadFile(path)
	require.NoError(t, err)
	text := string(data)

	assert.True(t, strings.HasPrefix(text, existing),
		"user content must remain at the top of the file")
	assert.Contains(t, text, InstructionsSentinel,
		"block must be appended below the user's content")
}

// TestAppendInstructions_SharedSentinelAcrossAdapters is the scenario
// that matters when two adapters target the same file (Codex and
// Opencode both write AGENTS.md). The second adapter must detect the
// first adapter's write via the shared InstructionsSentinel and skip,
// rather than duplicating the block. This is why the sentinel lives
// in the shared package, not each adapter.
func TestAppendInstructions_SharedSentinelAcrossAdapters(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "AGENTS.md")

	// Simulate Codex writing first.
	_, err := AppendInstructions(nil, path, InstructionsBody, InstructionsSentinel, ApplyOpts{})
	require.NoError(t, err)

	// Simulate Opencode running afterwards against the same repo.
	action, err := AppendInstructions(nil, path, InstructionsBody, InstructionsSentinel, ApplyOpts{})
	require.NoError(t, err)
	assert.Equal(t, ActionSkip, action.Action,
		"second adapter targeting the same file must skip, not append again")
}

// TestAppendInstructions_DryRunReportsAction verifies --dry-run never
// touches the filesystem and reports ActionWouldCreate / ActionWouldMerge
// correctly. Users rely on the planning output to preview what init
// will do; a silent write during dry-run would be a real footgun.
func TestAppendInstructions_DryRunReportsAction(t *testing.T) {
	dir := t.TempDir()
	newPath := filepath.Join(dir, "NEW.md")
	existingPath := filepath.Join(dir, "EXISTING.md")
	require.NoError(t, os.WriteFile(existingPath, []byte("preexisting\n"), 0o644))

	action, err := AppendInstructions(nil, newPath, InstructionsBody, InstructionsSentinel, ApplyOpts{DryRun: true})
	require.NoError(t, err)
	assert.Equal(t, ActionWouldCreate, action.Action)
	_, err = os.Stat(newPath)
	assert.True(t, os.IsNotExist(err), "dry-run must not create the file")

	action, err = AppendInstructions(nil, existingPath, InstructionsBody, InstructionsSentinel, ApplyOpts{DryRun: true})
	require.NoError(t, err)
	assert.Equal(t, ActionWouldMerge, action.Action)
	data, _ := os.ReadFile(existingPath)
	assert.Equal(t, "preexisting\n", string(data),
		"dry-run must not mutate an existing file")
}

// TestCursorMDCFrontmatter proves the MDC wrapper emits the two keys
// Cursor needs — `description` so users can see the rule in the UI
// and `alwaysApply: true` so it attaches to every chat turn. Without
// alwaysApply Cursor gates rules on keyword heuristics and the
// Gortex-preference block would fire only sporadically.
func TestCursorMDCFrontmatter(t *testing.T) {
	out := CursorMDCFrontmatter("BODY")
	assert.True(t, strings.HasPrefix(out, "---\n"),
		"MDC file must start with YAML frontmatter fence")
	assert.Contains(t, out, "alwaysApply: true",
		"MDC block must opt into always-apply so Cursor attaches it on every turn")
	assert.Contains(t, out, "description:")
	assert.Contains(t, out, "BODY")
}

// TestInstructionsBody_AdvertisesKeyTools is a smoke test: the shared
// body is the one artefact every agent relies on. A regression that
// silently emptied it (e.g. moving content into an unused variable)
// would leave `gortex init` installing a doc block that says nothing
// about preferring Gortex. We spot-check for tool names that anchor
// the MANDATORY message.
func TestInstructionsBody_AdvertisesKeyTools(t *testing.T) {
	for _, token := range []string{
		"search_symbols", "smart_context", "get_editing_context",
		"contracts", "find_usages", "graph_stats",
		// CPG-lite dataflow surface (flow_between / taint_paths).
		"flow_between", "taint_paths",
		"value_flow", "arg_of", "returns_to",
		// Infrastructure-as-graph surface (K8s / Kustomize / Dockerfile).
		"k8s_resources", "images", "kustomize",
		"uses_env", "configures", "mounts", "exposes", "depends_on",
		// Push diagnostics + code actions surface.
		"subscribe_diagnostics", "unsubscribe_diagnostics",
		"get_diagnostics", "get_code_actions", "apply_code_action",
		"fix_all_in_file", "notifications/diagnostics",
		// Structural code search.
		"search_ast", "error-not-wrapped", "sql-string-concat",
		"weak-crypto", "panic-in-library", "hardcoded-secret",
		"min_fan_in_of_enclosing_func",
		// Content compression (K9): the read_file tool and the
		// compress_bodies flag — the load-bearing tokens an agent
		// needs to discover the surface-only read mode.
		"read_file", "compress_bodies", "source_compressed",
		// MCP resources surface (bootstrap + analyzer rollups).
		"gortex://stats", "gortex://index-health", "gortex://workspace",
		"gortex://repos", "gortex://active-project",
		"gortex://report", "gortex://god-nodes", "gortex://surprises",
		"gortex://audit", "gortex://questions",
		"notifications/resources/updated",
	} {
		if !strings.Contains(InstructionsBody, token) {
			t.Errorf("InstructionsBody no longer mentions %q — a doc regression would ship to every adapter", token)
		}
	}
}

// TestGlobalInstructionsBody_AdvertisesKeyTools mirrors the smoke
// test above for the global block written by `gortex install` into
// ~/.claude/CLAUDE.md. It's the per-machine surface and gets the
// shorter dataflow callout — verify both tool names ship.
func TestGlobalInstructionsBody_AdvertisesKeyTools(t *testing.T) {
	for _, token := range []string{
		"search_symbols", "smart_context", "get_editing_context",
		"flow_between", "taint_paths",
		// Infrastructure surface — also written into the per-machine
		// block by `gortex install`.
		"k8s_resources", "images", "kustomize",
		// Structural code search — also in the per-machine block.
		"search_ast", "error-not-wrapped",
		// Content compression (K9): same load-bearing tokens as the
		// per-project block.
		"read_file", "compress_bodies", "source_compressed",
		// MCP resources surface (bootstrap + analyzer rollups).
		"gortex://stats", "gortex://index-health", "gortex://active-project",
		"gortex://report", "gortex://god-nodes",
		"notifications/resources/updated",
	} {
		if !strings.Contains(GlobalInstructionsBody, token) {
			t.Errorf("GlobalInstructionsBody no longer mentions %q", token)
		}
	}
}
