"""Tests for the `roam agent-export` command.

Validates:
- Text output contains expected sections
- JSON mode produces valid envelope
- --format claude/agents/cursor/codex/gemini produce expected headers
- --profile + --bundle target canonical AGENTS.md + provider overlays
- --write flag creates file in project root
- Deterministic output (same input -> same output)
- Minimal/empty index handling
"""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).parent))
from conftest import git_init, index_in_process, invoke_cli

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def small_project(tmp_path):
    """A minimal Python project with multiple files for agent-export testing."""
    repo = tmp_path / "myproject"
    repo.mkdir()
    (repo / ".gitignore").write_text(".roam/\n")

    src = repo / "src"
    src.mkdir()

    (src / "models.py").write_text(
        "class User:\n"
        "    def __init__(self, name):\n"
        "        self.name = name\n"
        "\n"
        "    def display(self):\n"
        "        return self.name.title()\n"
    )

    (src / "service.py").write_text(
        "from models import User\n"
        "\n"
        "def create_user(name):\n"
        "    return User(name)\n"
        "\n"
        "def greet(user):\n"
        "    return f'Hello, {user.display()}!'\n"
    )

    (src / "utils.py").write_text(
        "def add(a, b):\n    return a + b\n\ndef format_name(first, last):\n    return f'{first} {last}'\n"
    )

    tests_dir = repo / "tests"
    tests_dir.mkdir()

    (tests_dir / "test_models.py").write_text(
        "from models import User\n\ndef test_user():\n    u = User('alice')\n    assert u.display() == 'Alice'\n"
    )

    (repo / "pyproject.toml").write_text('[project]\nname = "myproject"\nversion = "1.0.0"\n')

    git_init(repo)
    return repo


@pytest.fixture
def indexed_small_project(small_project, monkeypatch):
    """An indexed small project ready for agent-export testing."""
    monkeypatch.chdir(small_project)
    out, rc = index_in_process(small_project)
    assert rc == 0, f"roam index failed:\n{out}"
    return small_project


@pytest.fixture
def empty_project(tmp_path, monkeypatch):
    """An indexed git repo with only a .gitignore (minimal/empty)."""
    repo = tmp_path / "empty"
    repo.mkdir()
    (repo / ".gitignore").write_text(".roam/\n")
    git_init(repo)
    monkeypatch.chdir(repo)
    out, rc = index_in_process(repo)
    assert rc == 0, f"roam index on empty repo failed:\n{out}"
    return repo


# ---------------------------------------------------------------------------
# 1. Basic output contains expected sections
# ---------------------------------------------------------------------------


class TestBasicOutput:
    def test_contains_expected_sections(self, indexed_small_project, cli_runner):
        """agent-export output should contain all major sections."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert result.exit_code == 0, f"Failed:\n{result.output}"

        output = result.output
        assert "VERDICT:" in output
        assert "## What this project is" in output
        assert "## Architecture" in output
        assert "## Key Files" in output
        assert "## Test Patterns" in output
        assert "## Common Tasks" in output

    def test_contains_language_info(self, indexed_small_project, cli_runner):
        """Output should mention Python as the language."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "Python" in result.output or "python" in result.output

    def test_contains_project_name(self, indexed_small_project, cli_runner):
        """Output should contain the project directory name."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "myproject" in result.output

    def test_contains_auto_marker(self, indexed_small_project, cli_runner):
        """Output should contain the auto-generated marker."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "Auto-generated by roam-code" in result.output

    def test_contains_build_commands(self, indexed_small_project, cli_runner):
        """Output should detect pip/pytest from pyproject.toml."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert result.exit_code == 0
        # pyproject.toml is present, so pip install -e . should be detected
        assert "pip install" in result.output or "pytest" in result.output

    def test_verdict_first(self, indexed_small_project, cli_runner):
        """First non-empty line of output should start with VERDICT:."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert result.exit_code == 0
        first_line = result.output.strip().split("\n")[0]
        assert first_line.startswith("VERDICT:")


# ---------------------------------------------------------------------------
# 2. JSON mode
# ---------------------------------------------------------------------------


class TestJsonMode:
    def test_json_produces_valid_envelope(self, indexed_small_project, cli_runner):
        """--json should produce a valid JSON envelope."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project, json_mode=True)
        assert result.exit_code == 0, f"Failed:\n{result.output}"

        data = json.loads(result.output)
        assert isinstance(data, dict)
        assert data["command"] == "agent-export"
        assert "summary" in data
        assert "verdict" in data["summary"]

    def test_json_contains_sections(self, indexed_small_project, cli_runner):
        """JSON output should contain the key data sections."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project, json_mode=True)
        data = json.loads(result.output)

        assert "languages" in data
        assert "stats" in data
        assert "key_files" in data
        assert "entry_points" in data
        assert "test_info" in data
        assert "project_name" in data

    def test_json_format_field(self, indexed_small_project, cli_runner):
        """JSON output should include the format field."""
        result = invoke_cli(
            cli_runner,
            ["agent-export", "--format", "agents"],
            cwd=indexed_small_project,
            json_mode=True,
        )
        data = json.loads(result.output)
        assert data["format"] == "agents"

    def test_json_has_schema(self, indexed_small_project, cli_runner):
        """JSON output should include schema versioning fields."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project, json_mode=True)
        data = json.loads(result.output)
        assert "schema" in data
        assert "schema_version" in data

    def test_json_sort_keys(self, indexed_small_project, cli_runner):
        """JSON keys should be sorted for deterministic output."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project, json_mode=True)
        # If keys are sorted, re-serializing with sort_keys should produce the same
        data = json.loads(result.output)
        reserialized = json.dumps(data, indent=2, default=str, sort_keys=True)
        # Compare parsed structure (not raw string, to avoid timestamp diffs)
        assert json.loads(reserialized) == data


# ---------------------------------------------------------------------------
# 3. Format variants
# ---------------------------------------------------------------------------


class TestFormatVariants:
    def test_claude_format_header(self, indexed_small_project, cli_runner):
        """--format claude should produce a simple project name header."""
        result = invoke_cli(cli_runner, ["agent-export", "--format", "claude"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "# myproject\n" in result.output

    def test_agents_format_header(self, indexed_small_project, cli_runner):
        """--format agents should produce an Agent Guide header."""
        result = invoke_cli(cli_runner, ["agent-export", "--format", "agents"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "Agent Guide" in result.output

    def test_cursor_format_header(self, indexed_small_project, cli_runner):
        """--format cursor should produce a Cursor Rules header."""
        result = invoke_cli(cli_runner, ["agent-export", "--format", "cursor"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "Cursor Rules" in result.output

    def test_codex_format_header(self, indexed_small_project, cli_runner):
        """--format codex should produce a Codex header."""
        result = invoke_cli(cli_runner, ["agent-export", "--format", "codex"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "Codex Instructions" in result.output

    def test_gemini_format_header(self, indexed_small_project, cli_runner):
        """--format gemini should produce a Gemini header."""
        result = invoke_cli(cli_runner, ["agent-export", "--format", "gemini"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "Gemini Instructions" in result.output

    def test_profile_selects_default_format(self, indexed_small_project, cli_runner):
        """--profile codex should select codex format when --format is omitted."""
        result = invoke_cli(cli_runner, ["agent-export", "--profile", "codex"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "Codex Instructions" in result.output

    def test_cursor_format_is_concise(self, indexed_small_project, cli_runner):
        """--format cursor should skip roam-specific commands in Common Tasks."""
        result = invoke_cli(cli_runner, ["agent-export", "--format", "cursor"], cwd=indexed_small_project)
        assert result.exit_code == 0
        # Cursor format should NOT include roam commands
        assert "roam search" not in result.output
        assert "roam preflight" not in result.output

    def test_different_formats_produce_different_output(self, indexed_small_project, cli_runner):
        """claude, agents, and cursor should produce distinct outputs."""
        results = {}
        for fmt in ("claude", "agents", "cursor"):
            r = invoke_cli(cli_runner, ["agent-export", "--format", fmt], cwd=indexed_small_project)
            assert r.exit_code == 0
            results[fmt] = r.output

        # All three should be different
        assert results["claude"] != results["agents"]
        assert results["claude"] != results["cursor"]
        assert results["agents"] != results["cursor"]


# ---------------------------------------------------------------------------
# 4. --write flag
# ---------------------------------------------------------------------------


class TestWriteFlag:
    def test_write_creates_claude_md(self, indexed_small_project, cli_runner):
        """--write should create CLAUDE.md in the project root."""
        result = invoke_cli(cli_runner, ["agent-export", "--write"], cwd=indexed_small_project)
        assert result.exit_code == 0

        claude_path = indexed_small_project / "CLAUDE.md"
        assert claude_path.exists(), "CLAUDE.md should be created"
        content = claude_path.read_text(encoding="utf-8")
        assert "Auto-generated by roam-code" in content
        assert "## What this project is" in content

    def test_write_agents_format(self, indexed_small_project, cli_runner):
        """--write --format agents should create AGENTS.md."""
        result = invoke_cli(cli_runner, ["agent-export", "--write", "--format", "agents"], cwd=indexed_small_project)
        assert result.exit_code == 0

        agents_path = indexed_small_project / "AGENTS.md"
        assert agents_path.exists(), "AGENTS.md should be created"

    def test_write_cursor_format(self, indexed_small_project, cli_runner):
        """--write --format cursor should create .cursorrules."""
        result = invoke_cli(cli_runner, ["agent-export", "--write", "--format", "cursor"], cwd=indexed_small_project)
        assert result.exit_code == 0

        cursor_path = indexed_small_project / ".cursorrules"
        assert cursor_path.exists(), ".cursorrules should be created"

    def test_write_bundle_codex_profile(self, indexed_small_project, cli_runner):
        """--profile codex --bundle --write should create AGENTS.md + CODEX.md."""
        result = invoke_cli(
            cli_runner,
            ["agent-export", "--profile", "codex", "--bundle", "--write"],
            cwd=indexed_small_project,
        )
        assert result.exit_code == 0
        assert (indexed_small_project / "AGENTS.md").exists()
        assert (indexed_small_project / "CODEX.md").exists()
        assert "Wrote bundle:" in result.output

    def test_write_bundle_gemini_profile(self, indexed_small_project, cli_runner):
        """--profile gemini --bundle --write should create AGENTS.md + GEMINI.md."""
        result = invoke_cli(
            cli_runner,
            ["agent-export", "--profile", "gemini", "--bundle", "--write"],
            cwd=indexed_small_project,
        )
        assert result.exit_code == 0
        assert (indexed_small_project / "AGENTS.md").exists()
        assert (indexed_small_project / "GEMINI.md").exists()

    def test_bundle_requires_write_or_json(self, indexed_small_project, cli_runner):
        """--bundle without --write/--json should fail with usage error."""
        result = invoke_cli(
            cli_runner,
            ["agent-export", "--profile", "codex", "--bundle"],
            cwd=indexed_small_project,
        )
        assert result.exit_code != 0
        assert "--bundle requires --write" in result.output

    def test_bundle_not_allowed_with_output_multi_format(self, indexed_small_project, cli_runner):
        """--output with --bundle should fail when bundle resolves to multiple files."""
        out_path = str(indexed_small_project / "out.md")
        result = invoke_cli(
            cli_runner,
            ["agent-export", "--profile", "codex", "--bundle", "--output", out_path],
            cwd=indexed_small_project,
        )
        assert result.exit_code != 0
        assert "--output supports a single format" in result.output

    def test_write_reports_size(self, indexed_small_project, cli_runner):
        """--write should report the file size and section count."""
        result = invoke_cli(cli_runner, ["agent-export", "--write"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "Wrote CLAUDE.md" in result.output
        assert "KB" in result.output
        assert "sections" in result.output

    def test_write_preserves_manual_content(self, indexed_small_project, cli_runner):
        """--write should preserve manual additions outside auto-generated markers."""
        claude_path = indexed_small_project / "CLAUDE.md"

        # First write
        result = invoke_cli(cli_runner, ["agent-export", "--write"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert claude_path.exists()

        # Add manual content after the auto-generated section
        content = claude_path.read_text(encoding="utf-8")
        content += "\n## My Custom Section\n\nThis is manual content.\n"
        claude_path.write_text(content, encoding="utf-8")

        # Re-run --write
        result = invoke_cli(cli_runner, ["agent-export", "--write"], cwd=indexed_small_project)
        assert result.exit_code == 0

        # Check manual content is preserved
        updated = claude_path.read_text(encoding="utf-8")
        assert "## My Custom Section" in updated
        assert "This is manual content." in updated
        # Auto-generated content should still be there
        assert "Auto-generated by roam-code" in updated

    def test_output_flag_custom_path(self, indexed_small_project, cli_runner):
        """-o should write to a custom path."""
        out_path = str(indexed_small_project / "custom_agents.md")
        result = invoke_cli(cli_runner, ["agent-export", "-o", out_path], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert os.path.exists(out_path)
        with open(out_path, encoding="utf-8") as _fh:
            content = _fh.read()
        assert "Auto-generated by roam-code" in content


# ---------------------------------------------------------------------------
# 5. Deterministic output
# ---------------------------------------------------------------------------


class TestDeterministic:
    def test_same_input_same_output(self, indexed_small_project, cli_runner):
        """Running agent-export twice should produce identical output."""
        r1 = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        r2 = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert r1.exit_code == 0
        assert r2.exit_code == 0

        # Strip the VERDICT line (may contain dynamic section count)
        # and compare the markdown body
        body1 = "\n".join(r1.output.strip().split("\n")[2:])
        body2 = "\n".join(r2.output.strip().split("\n")[2:])
        assert body1 == body2

    def test_json_deterministic(self, indexed_small_project, cli_runner):
        """JSON output should be deterministic (ignoring timestamp)."""
        r1 = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project, json_mode=True)
        r2 = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project, json_mode=True)
        d1 = json.loads(r1.output)
        d2 = json.loads(r2.output)

        # Remove timestamp and index_age_s which are inherently dynamic
        for d in (d1, d2):
            d.pop("timestamp", None)
            d.pop("index_age_s", None)
            if "_meta" in d:
                d["_meta"].pop("timestamp", None)
                d["_meta"].pop("index_age_s", None)

        assert d1 == d2


# ---------------------------------------------------------------------------
# 6. Empty/minimal index
# ---------------------------------------------------------------------------


class TestEmptyProject:
    def test_empty_project_succeeds(self, empty_project, cli_runner):
        """agent-export on an empty project should not crash."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=empty_project)
        assert result.exit_code == 0
        assert "VERDICT:" in result.output

    def test_empty_project_json(self, empty_project, cli_runner):
        """JSON mode on an empty project should produce valid output."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=empty_project, json_mode=True)
        assert result.exit_code == 0
        data = json.loads(result.output)
        assert data["command"] == "agent-export"
        assert data["summary"]["files"] == 0 or isinstance(data["summary"]["files"], int)

    def test_empty_project_write(self, empty_project, cli_runner):
        """--write on an empty project should still create the file."""
        result = invoke_cli(cli_runner, ["agent-export", "--write"], cwd=empty_project)
        assert result.exit_code == 0
        claude_path = empty_project / "CLAUDE.md"
        assert claude_path.exists()


# ---------------------------------------------------------------------------
# 7. Hotspots and entry points
# ---------------------------------------------------------------------------


class TestDataSections:
    def test_entry_points_present(self, indexed_small_project, cli_runner):
        """Entry points section should list files."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert result.exit_code == 0
        # We expect some entry points in the output
        if "## Entry Points" in result.output:
            # If present, should have at least one file
            idx = result.output.index("## Entry Points")
            section = result.output[idx : idx + 500]
            assert "`" in section  # file paths are in backticks

    def test_test_patterns_present(self, indexed_small_project, cli_runner):
        """Test patterns section should mention test directory and patterns."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert result.exit_code == 0
        assert "## Test Patterns" in result.output
        # Should detect the tests/ directory
        assert "tests/" in result.output or "test" in result.output.lower()

    def test_directory_layout_present(self, indexed_small_project, cli_runner):
        """Directory layout should show top-level dirs."""
        result = invoke_cli(cli_runner, ["agent-export"], cwd=indexed_small_project)
        assert result.exit_code == 0
        if "### Directory layout" in result.output:
            assert "src/" in result.output


# ---------------------------------------------------------------------------
# 8. Help text
# ---------------------------------------------------------------------------


class TestHelp:
    def test_help_flag(self, cli_runner):
        """agent-export --help should work."""
        result = invoke_cli(cli_runner, ["agent-export", "--help"])
        assert result.exit_code == 0
        assert "Generate" in result.output
        assert "--format" in result.output
        assert "--profile" in result.output
        assert "--bundle" in result.output
        assert "--write" in result.output
