"""End-to-end test for single-project full flow.

Tests the complete Lattice workflow:
1. lattice init - Initialize a project
2. Ingest multiple session messages
3. lattice evolve (mock LLM) - Run evolution
4. Verify rules/*.md generated reasonable rules
5. lattice search returns relevant results
6. MCP server starts and server_instructions includes new rules
7. Second evolve (incremental) processes only new sessions
8. Test both modes: FTS5-only and with embedding configured

Reference: RFC-002
"""

import tempfile
from pathlib import Path

import pytest
from returns.result import Success

from lattice import Client, PatternType
from lattice.core.config import get_default_config
from lattice.core.types.enums import PatternType, ProposalAction, Role
from lattice.core.types.evidence import CotPhases
from lattice.core.types.proposal import Proposal
from lattice.shell.evolution import (
    EvolutionConfig,
    apply_proposal,
    query_pending_sessions,
    update_last_evolved_at,
    write_proposal,
)
from lattice.shell.schema import create_store
from lattice.shell.store import generate_session_id, insert_log


class TestSingleProjectE2E:
    """End-to-end test for single project flow."""

    @pytest.fixture
    def project_dir(self, tmp_path: Path) -> Path:
        """Create a temporary project directory with .lattice initialized."""
        project = tmp_path / "test_project"
        project.mkdir()
        lattice_dir = project / ".lattice"
        lattice_dir.mkdir()
        (lattice_dir / "rules").mkdir()
        (lattice_dir / "drift" / "proposals").mkdir(parents=True)
        (lattice_dir / "drift" / "traces").mkdir(parents=True)
        (lattice_dir / "backups").mkdir()

        # Create default config (no embedding - FTS5 only mode)
        (lattice_dir / "config.toml").write_text('[compiler]\nmodel = "test"\n')

        # Create store.db
        result = create_store(lattice_dir / "store.db")
        assert isinstance(result, Success)
        result.unwrap().close()

        # Create compiler prompt template
        data_dir = project / "data"
        data_dir.mkdir()
        prompt_template = data_dir / "compiler_prompt.md"
        prompt_template.write_text(
            "<triage>\n{sessions}\n</triage>\n"
            "<cross_ref>\n{current_rules}\n</cross_ref>\n"
            "<synthesis>\n</synthesis>\n"
            "<review>\n</review>\n"
        )

        return project

    def _ingest_session(
        self, project_dir: Path, session_id: str, messages: list[tuple[str, str]]
    ) -> None:
        """Ingest a conversation session into the store."""
        lattice_dir = project_dir / ".lattice"
        store_path = lattice_dir / "store.db"

        result = create_store(store_path)
        assert isinstance(result, Success)
        conn = result.unwrap()

        try:
            for role_str, content in messages:
                role = Role.USER if role_str == "user" else Role.ASSISTANT
                insert_result = insert_log(conn, session_id, role, content, None)
                assert isinstance(insert_result, Success)
        finally:
            conn.close()

    def test_full_flow_fts5_only(self, project_dir: Path) -> None:
        """Test full flow with FTS5-only mode (no embedding)."""
        lattice_dir = project_dir / ".lattice"
        store_path = lattice_dir / "store.db"

        # Step 1: Verify project is initialized
        assert (lattice_dir / "store.db").exists()
        assert (lattice_dir / "rules").exists()

        # Step 2: Ingest multiple sessions
        session_1 = generate_session_id()
        self._ingest_session(
            project_dir,
            session_1,
            [
                ("user", "I need to implement user authentication"),
                ("assistant", "I'll help you set up JWT-based authentication."),
                ("user", "Make sure to validate token expiry"),
                ("assistant", "Added JWT expiry validation with 24-hour default."),
            ],
        )

        session_2 = generate_session_id()
        self._ingest_session(
            project_dir,
            session_2,
            [
                ("user", "Add rate limiting to the API"),
                ("assistant", "Implemented rate limiting with 100 req/min per client."),
            ],
        )

        session_3 = generate_session_id()
        self._ingest_session(
            project_dir,
            session_3,
            [
                ("user", "The JWT validation is failing intermittently"),
                ("assistant", "Found race condition in token verification. Fixed."),
            ],
        )

        # Step 3: Search should find relevant results (FTS5 only)
        client = Client(project_dir)
        result = client.search("JWT authentication")
        assert isinstance(result, Success)
        results = result.unwrap()
        assert len(results) >= 1, "Should find JWT-related messages"

        # Step 4: Mock evolution - write and apply proposal
        mock_proposal = Proposal(
            proposal_id="test-1",
            pattern=PatternType.CONVENTION,
            action=ProposalAction.ADD,
            title="JWT Token Validation",
            content="Always validate JWT token expiry before processing requests.",
            evidence_session_ids=[session_1, session_3],
        )

        mock_cot = CotPhases(
            triage="Analyzed 3 sessions about authentication",
            cross_ref="No existing authentication rules",
            synthesis="Need JWT validation convention",
            review="Proposal validated",
        )

        # Write proposal via evolution module
        proposal_result = write_proposal(
            project_dir, [mock_proposal], mock_cot, sessions_processed=3
        )
        assert isinstance(proposal_result, Success)

        # Step 5: Apply proposal and verify rules created
        proposal_paths = proposal_result.unwrap()
        proposal_file = proposal_paths[0]  # Relative path like drift/proposals/...

        # Apply the proposal (now handles both bare filename and relative path)
        apply_result = apply_proposal(project_dir, proposal_file)
        assert isinstance(apply_result, Success)

        # Verify rule file created
        rules_dir = lattice_dir / "rules"
        rule_files = list(rules_dir.glob("*.md"))
        assert len(rule_files) >= 1, "Should have created a rule file"

        # Verify rule content
        rule_content = rule_files[0].read_text()
        assert "JWT" in rule_content or "validation" in rule_content.lower()

        # Verify rule file created
        rules_dir = lattice_dir / "rules"
        rule_files = list(rules_dir.glob("*.md"))
        assert len(rule_files) >= 1, "Should have created a rule file"

        # Verify rule content
        rule_content = rule_files[0].read_text()
        assert "JWT" in rule_content or "validation" in rule_content.lower()

        # Step 6: Verify get_instincts includes new rules
        instincts = client.get_instincts()
        assert len(instincts) > 0, "Should have rules in instincts"

        # Step 7: Verify evolution tracking works
        conn_result = create_store(store_path)
        assert isinstance(conn_result, Success)
        conn = conn_result.unwrap()

        try:
            # Update last_evolved_at metadata
            from datetime import datetime, timezone

            update_result = update_last_evolved_at(
                conn, datetime.now(timezone.utc).isoformat()
            )
            assert isinstance(update_result, Success)
        finally:
            conn.close()

    def test_search_relevance(self, project_dir: Path) -> None:
        """Test search returns relevant results."""
        # Ingest varied content
        session_id = generate_session_id()
        self._ingest_session(
            project_dir,
            session_id,
            [
                ("user", "How do I configure the database connection?"),
                ("assistant", "Use the DATABASE_URL environment variable."),
                ("user", "What about connection pooling?"),
                ("assistant", "Set POOL_SIZE in config.toml under [database]."),
            ],
        )

        client = Client(project_dir)

        # Test various search queries
        db_result = client.search("database")
        assert isinstance(db_result, Success)
        db_results = db_result.unwrap()
        assert len(db_results) >= 1

        pool_result = client.search("pooling")
        assert isinstance(pool_result, Success)

        # Note: FTS5 has special syntax for "." so search without it
        config_result = client.search("config")
        assert isinstance(config_result, Success)

    def test_propose_rule_via_sdk(self, project_dir: Path) -> None:
        """Test proposing rules via the Python SDK."""
        # Create drift/proposals directory
        proposals_dir = project_dir / ".lattice" / "drift" / "proposals"
        proposals_dir.mkdir(parents=True, exist_ok=True)

        client = Client(project_dir)

        # Propose a rule
        result = client.propose_rule(
            pattern=PatternType.CONVENTION,
            observation="Always validate input before processing",
            evidence=["session-1", "session-2"],
            suggested_action="add-validation-rule",
        )

        assert isinstance(result, Success)
        proposal_path = result.unwrap()
        assert "proposal_" in proposal_path

        # Verify file created
        full_path = project_dir / proposal_path
        assert full_path.exists()

        content = full_path.read_text()
        assert "convention" in content
        assert "validation" in content

    def test_multiple_sessions_incremental_evolution(self, project_dir: Path) -> None:
        """Test incremental evolution processes only new sessions after update."""
        lattice_dir = project_dir / ".lattice"
        store_path = lattice_dir / "store.db"

        # Use single connection for consistency
        conn_result = create_store(store_path)
        assert isinstance(conn_result, Success)
        conn = conn_result.unwrap()

        try:
            # Ingest first batch using same connection
            for i in range(3):
                session_id = generate_session_id()
                for role_str, content in [
                    ("user", f"Batch 1 - Message {i}"),
                    ("assistant", f"Batch 1 - Response {i}"),
                ]:
                    role = Role.USER if role_str == "user" else Role.ASSISTANT
                    insert_result = insert_log(conn, session_id, role, content, None)
                    assert isinstance(insert_result, Success)

            # Check pending sessions
            pending_result = query_pending_sessions(conn, EvolutionConfig())
            assert isinstance(pending_result, Success)
            pending = pending_result.unwrap()
            assert pending.total_pending == 3, (
                f"Should have 3 pending, got {pending.total_pending}"
            )

            # Mark as evolved
            from datetime import datetime, timezone

            evolved_ts = datetime.now(timezone.utc).isoformat()
            update_last_evolved_at(conn, evolved_ts)

            # Ingest second batch (after the marker)
            for i in range(2):
                session_id = generate_session_id()
                for role_str, content in [
                    ("user", f"Batch 2 - Message {i}"),
                    ("assistant", f"Batch 2 - Response {i}"),
                ]:
                    role = Role.USER if role_str == "user" else Role.ASSISTANT
                    insert_result = insert_log(conn, session_id, role, content, None)
                    assert isinstance(insert_result, Success)

            # Should only see new sessions (2 from Batch 2)
            pending_result = query_pending_sessions(conn, EvolutionConfig())
            assert isinstance(pending_result, Success)
            pending = pending_result.unwrap()
            assert pending.total_pending == 2, (
                f"Should have only 2 new sessions, got {pending.total_pending}"
            )
        finally:
            conn.close()


class TestEdgeCases:
    """Test edge cases and error handling."""

    def test_client_with_global_rules(self, tmp_path: Path) -> None:
        """Test client can access global rules."""
        project = tmp_path / "project"
        project.mkdir()
        lattice_dir = project / ".lattice"
        lattice_dir.mkdir()

        global_dir = tmp_path / "global"
        global_rules = global_dir / "rules"
        global_rules.mkdir(parents=True)
        (global_rules / "global_convention.md").write_text(
            "# Global Convention\n\nWrite clean code.\n"
        )

        client = Client(project, global_path=global_dir)
        instincts = client.get_instincts()

        assert "Global Convention" in instincts
        assert "clean code" in instincts

    def test_rules_not_duplicated(self, tmp_path: Path) -> None:
        """Test that rules aren't duplicated in instincts."""
        project = tmp_path / "project"
        project.mkdir()
        lattice_dir = project / ".lattice"
        lattice_dir.mkdir()
        (lattice_dir / "rules").mkdir()
        (lattice_dir / "config.toml").write_text('[compiler]\nmodel = "test"\n')

        result = create_store(lattice_dir / "store.db")
        assert isinstance(result, Success)
        result.unwrap().close()

        rules_dir = lattice_dir / "rules"

        # Create a rule
        (rules_dir / "rule.md").write_text("# Test Rule\n\nContent here.\n")

        client = Client(project)
        instincts = client.get_instincts()

        # Count occurrences
        count = instincts.count("# Test Rule")
        assert count == 1, "Rule should appear only once"
