"""MCP server integration tests."""

from __future__ import annotations

import asyncio
import sqlite3
import tempfile
from pathlib import Path
from typing import Generator

import pytest
from returns.result import Success

from lattice.shell.server import LatticeMCPServer


@pytest.fixture
def tmp_project() -> Generator[Path, None, None]:
    """Create a temporary project directory with .lattice/."""
    with tempfile.TemporaryDirectory() as tmpdir:
        project_path = Path(tmpdir)
        lattice_dir = project_path / ".lattice"
        lattice_dir.mkdir()
        (lattice_dir / "rules").mkdir()
        (lattice_dir / "drift").mkdir()
        (lattice_dir / "drift" / "proposals").mkdir()
        (lattice_dir / "drift" / "traces").mkdir()
        (lattice_dir / "backups").mkdir()
        yield project_path


@pytest.fixture
def tmp_store(tmp_project: Path) -> Generator[sqlite3.Connection, None, None]:
    """Create a temporary store.db with schema."""
    from lattice.shell.schema import create_store

    store_path = tmp_project / ".lattice" / "store.db"
    result = create_store(store_path)
    conn = result.unwrap()
    yield conn
    conn.close()


class TestMCPServerInit:
    """Tests for MCP server initialization."""

    def test_server_initializes(self) -> None:
        """Server initializes with default instructions."""
        server = LatticeMCPServer()
        assert server.server.name == "lattice"
        assert server.config is not None


class TestLatticeSearchTool:
    """Tests for lattice_search MCP tool."""

    def test_search_returns_results(
        self, tmp_project: Path, tmp_store: sqlite3.Connection
    ) -> None:
        """Search returns ingested content."""
        import os

        from lattice.core.types.enums import Role
        from lattice.shell.store import insert_log

        old_cwd = os.getcwd()
        os.chdir(tmp_project)

        try:
            # Insert test data
            insert_log(tmp_store, "session-1", Role.USER, "Test search content")

            server = LatticeMCPServer()
            result = asyncio.run(server._handle_search({"query": "search"}))

            assert len(result) == 1
            assert "Found" in result[0]["text"]

        finally:
            os.chdir(old_cwd)

    def test_search_empty_query(self) -> None:
        """Search with empty query returns error."""
        server = LatticeMCPServer()
        result = asyncio.run(server._handle_search({"query": ""}))

        assert len(result) == 1
        assert "Error" in result[0]["text"]


class TestLogTurnTool:
    """Tests for log_turn MCP tool."""

    def test_log_turn_writes_data(
        self, tmp_project: Path, tmp_store: sqlite3.Connection
    ) -> None:
        """log_turn writes log entry to store.db."""
        import os

        old_cwd = os.getcwd()
        os.chdir(tmp_project)

        try:
            server = LatticeMCPServer()
            result = asyncio.run(
                server._handle_log_turn(
                    {
                        "role": "user",
                        "content": "Hello from MCP",
                    }
                )
            )

            assert len(result) == 1
            assert "Logged turn" in result[0]["text"]

            # Verify session_id was set
            assert server.session_id is not None

            # Verify data in DB
            cursor = tmp_store.execute("SELECT COUNT(*) FROM logs")
            count = cursor.fetchone()[0]
            assert count >= 1

        finally:
            os.chdir(old_cwd)


class TestGetInstinctsTool:
    """Tests for get_instincts MCP tool."""

    def test_get_instincts_returns_instructions(self, tmp_project: Path) -> None:
        """get_instincts returns server instructions."""
        import os

        old_cwd = os.getcwd()
        os.chdir(tmp_project)

        try:
            server = LatticeMCPServer()
            result = asyncio.run(server._handle_get_instincts({}))

            assert len(result) == 1
            # Either rules or default instructions should contain "Lattice"
            assert "Lattice" in result[0]["text"] or result[0]["text"] == ""

        finally:
            os.chdir(old_cwd)


class TestProposeRuleTool:
    """Tests for propose_rule MCP tool."""

    def test_propose_rule_creates_proposal(self, tmp_project: Path) -> None:
        """propose_rule creates proposal file."""
        import os

        old_cwd = os.getcwd()
        os.chdir(tmp_project)

        try:
            server = LatticeMCPServer()
            result = asyncio.run(
                server._handle_propose_rule(
                    {
                        "pattern": "convention",
                        "observation": "Always use type hints",
                        "evidence": "session-1",
                        "suggested_action": "add",
                    }
                )
            )

            assert len(result) == 1
            assert "Proposal created" in result[0]["text"]

            # Verify proposal file exists (under .lattice/)
            proposals_dir = tmp_project / ".lattice" / "drift" / "proposals"
            files = list(proposals_dir.glob("*.md"))
            assert len(files) > 0

        finally:
            os.chdir(old_cwd)

    def test_propose_rule_validates_pattern(self) -> None:
        """propose_rule validates pattern enum."""
        server = LatticeMCPServer()
        result = asyncio.run(
            server._handle_propose_rule(
                {
                    "pattern": "invalid-pattern",
                    "observation": "Test",
                    "evidence": "",
                    "suggested_action": "add",
                }
            )
        )

        assert len(result) == 1
        assert "Error" in result[0]["text"]


class TestGetStatusTool:
    """Tests for get_status MCP tool."""

    def test_get_status_returns_info(
        self, tmp_project: Path, tmp_store: sqlite3.Connection
    ) -> None:
        """get_status returns status info."""
        import os

        old_cwd = os.getcwd()
        os.chdir(tmp_project)

        try:
            server = LatticeMCPServer()
            result = asyncio.run(server._handle_get_status({}))

            assert len(result) == 1
            assert "Lattice Status" in result[0]["text"]
            assert "Rule Token Budget" in result[0]["text"]

        finally:
            os.chdir(old_cwd)


class TestMCPResources:
    """Tests for MCP resource handlers."""

    def test_list_resources_empty(self, tmp_project: Path) -> None:
        """list_resources returns empty list when no files."""
        import os

        old_cwd = os.getcwd()
        os.chdir(tmp_project)

        try:
            server = LatticeMCPServer()
            # The list_resources is registered as a handler, we can't call it directly
            # Just verify server initializes properly
            assert server.server.name == "lattice"

        finally:
            os.chdir(old_cwd)

    def test_read_resource_rule_file(self, tmp_project: Path) -> None:
        """read_resource reads rule file content."""
        import os

        old_cwd = os.getcwd()
        os.chdir(tmp_project)

        try:
            # Create a test rule
            rule_file = tmp_project / ".lattice" / "rules" / "test.md"
            rule_file.write_text("# Test Rule\n\nContent here.")

            server = LatticeMCPServer()

            # Verify the rule file exists
            assert rule_file.exists()

        finally:
            os.chdir(old_cwd)

    def test_read_resource_proposal_file(self, tmp_project: Path) -> None:
        """read_resource reads proposal file content."""
        import os

        old_cwd = os.getcwd()
        os.chdir(tmp_project)

        try:
            # Create a test proposal
            proposal_file = tmp_project / "drift" / "proposals" / "test_proposal.md"
            proposal_file.parent.mkdir(parents=True, exist_ok=True)
            proposal_file.write_text("# Test Proposal\n\nProposal content.")

            server = LatticeMCPServer()

            # Verify the proposal file exists
            assert proposal_file.exists()

        finally:
            os.chdir(old_cwd)

    def test_validate_safe_path_valid(self, tmp_project: Path) -> None:
        """_validate_safe_path accepts valid filenames."""
        from lattice.shell.server import _validate_safe_path

        rules_dir = tmp_project / ".lattice" / "rules"
        (rules_dir / "valid.md").write_text("content")

        result = _validate_safe_path(rules_dir, "valid.md")
        assert isinstance(result, Success)
        resolved = result.unwrap()
        assert resolved.name == "valid.md"

    def test_validate_safe_path_traversal_attack(self, tmp_project: Path) -> None:
        """_validate_safe_path rejects path traversal attempts."""
        from returns.result import Failure

        from lattice.shell.server import _validate_safe_path

        rules_dir = tmp_project / ".lattice" / "rules"

        # Various traversal patterns
        traversal_attempts = [
            "../secret.txt",
            "../../etc/passwd",
            "subdir/../../../etc/passwd",
            "..%2F..%2Fetc%2Fpasswd",  # URL encoded (still contains ..)
        ]

        for attempt in traversal_attempts:
            result = _validate_safe_path(rules_dir, attempt)
            assert isinstance(result, Failure), f"Should reject: {attempt}"

    def test_validate_safe_path_absolute_path(self, tmp_project: Path) -> None:
        """_validate_safe_path rejects absolute paths."""
        from returns.result import Failure

        from lattice.shell.server import _validate_safe_path

        rules_dir = tmp_project / ".lattice" / "rules"

        result = _validate_safe_path(rules_dir, "/etc/passwd")
        assert isinstance(result, Failure)


class TestCaptureMode:
    """Tests for capture mode configuration."""

    def test_capture_mode_default_is_passive(self) -> None:
        """Default capture mode is passive."""
        from lattice.core.types.config import CaptureConfig

        config = CaptureConfig()
        assert config.mode == "passive"
        assert not config.is_active()

    def test_capture_mode_active(self) -> None:
        """Active capture mode returns is_active True."""
        from lattice.core.types.config import CaptureConfig

        config = CaptureConfig(mode="active")
        assert config.mode == "active"
        assert config.is_active()

    def test_capture_mode_invalid_defaults_to_passive(self) -> None:
        """Invalid mode defaults to passive during validation."""
        from lattice.core.config import _validate_capture

        config = _validate_capture({"mode": "invalid"})
        assert config.mode == "passive"

    def test_passive_mode_excludes_log_turn_tool(self) -> None:
        """Passive mode server does not expose log_turn tool."""
        server = LatticeMCPServer()
        # Default is passive
        assert server.config.capture.mode == "passive"

        tools = server._get_tool_definitions()
        tool_names = [t.name for t in tools]

        assert "lattice_search" in tool_names
        assert "log_turn" not in tool_names
        assert "get_instincts" in tool_names
        assert "propose_rule" in tool_names
        assert "get_status" in tool_names

    def test_active_mode_includes_log_turn_tool(self, tmp_project: Path) -> None:
        """Active mode server exposes log_turn tool."""
        import os

        from lattice.core.types.config import CaptureConfig, CompilerConfig, Config

        old_cwd = os.getcwd()
        os.chdir(tmp_project)

        try:
            # Create server with active capture mode
            server = LatticeMCPServer()
            server.config = Config(
                compiler=CompilerConfig(model="test", api_key_env="TEST"),
                capture=CaptureConfig(mode="active"),
            )

            tools = server._get_tool_definitions()
            tool_names = [t.name for t in tools]

            assert "lattice_search" in tool_names
            assert "log_turn" in tool_names
            assert "get_instincts" in tool_names

        finally:
            os.chdir(old_cwd)

    def test_passive_mode_log_turn_not_in_tool_list(self) -> None:
        """Passive mode server does not include log_turn in tool list."""
        server = LatticeMCPServer()
        # Default is passive
        assert server.config.capture.mode == "passive"

        # log_turn should not be in tool definitions
        tools = server._get_tool_definitions()
        tool_names = [t.name for t in tools]
        assert "log_turn" not in tool_names

    def test_passive_mode_instructions_exclude_log_turn(self) -> None:
        """Server instructions exclude log_turn in passive mode."""
        from lattice.core.types.rule import Rule

        rules = [Rule(file_path="test.md", title="Test", content="Content")]
        from lattice.core.rules import assemble_instincts

        instructions = assemble_instincts(rules, capture_active=False)

        assert "lattice_search" in instructions
        assert "log_turn" not in instructions

    def test_active_mode_instructions_include_log_turn(self) -> None:
        """Server instructions include log_turn in active mode."""
        from lattice.core.types.rule import Rule

        rules = [Rule(file_path="test.md", title="Test", content="Content")]
        from lattice.core.rules import assemble_instincts

        instructions = assemble_instincts(rules, capture_active=True)

        assert "lattice_search" in instructions
        assert "log_turn" in instructions


class TestCaptureModeOverride:
    """Tests for capture_mode constructor override (Phase 1: capture-mode-cli)."""

    def test_capture_mode_override_active(self) -> None:
        """capture_mode='active' overrides config.toml passive default."""
        server = LatticeMCPServer(capture_mode="active")
        assert server.config.capture.is_active()

    def test_capture_mode_override_passive(self) -> None:
        """capture_mode='passive' overrides any active config."""
        server = LatticeMCPServer(capture_mode="passive")
        assert not server.config.capture.is_active()

    def test_capture_mode_override_none_uses_config(self) -> None:
        """capture_mode=None falls back to config.toml (default: passive)."""
        server = LatticeMCPServer(capture_mode=None)
        # Default config has passive mode
        assert not server.config.capture.is_active()

    def test_capture_mode_override_active_exposes_log_turn(self) -> None:
        """capture_mode='active' override exposes log_turn tool."""
        server = LatticeMCPServer(capture_mode="active")
        tool_names = [t.name for t in server._get_tool_definitions()]
        assert "log_turn" in tool_names

    def test_capture_mode_override_passive_hides_log_turn(self) -> None:
        """capture_mode='passive' override hides log_turn tool."""
        server = LatticeMCPServer(capture_mode="passive")
        tool_names = [t.name for t in server._get_tool_definitions()]
        assert "log_turn" not in tool_names
