"""
Integration tests for Global Evolution.

Tests the two-phase global compiler flow and related functionality.

Reference: RFC-002 §4.4 Global Evolution
"""

from __future__ import annotations

import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from returns.result import Failure, Success

from lattice.core.global_compiler import (
    GlobalCandidatePattern,
    GlobalProposal,
    assemble_global_phase1_prompt,
    assemble_global_phase2_prompt,
    cosine_similarity,
    format_global_proposal_content,
    format_global_trace_content,
    parse_candidate_patterns,
    parse_global_proposals,
)
from lattice.core.global_compiler_parse import (
    extract_cross_ref_content,
    extract_synthesis_content,
    extract_triage_content,
)
from lattice.shell.global_evolution import (
    EvidenceRecord,
    GlobalPhase1Output,
    GlobalPhase2Output,
    ProjectRegistration,
    compute_embedding_clusters,
    mark_rule_promoted,
    query_project_evidence,
    read_global_rules,
    run_global_phase1,
    run_global_phase2,
    scan_project_rules,
    scan_project_traces,
    write_evidence_embedding,
    write_evidence_to_global_db,
    write_global_evolution_output,
    write_rule_evidence_associations,
)
from lattice.shell.schema import create_global_store


class TestGlobalCompilerCoreFunctions:
    """Tests for core global compiler functions."""

    def test_cosine_similarity_identical_vectors(self):
        """Identical vectors should have similarity 1.0."""
        result = cosine_similarity([1.0, 0.0, 0.0], [1.0, 0.0, 0.0])
        assert result == 1.0

    def test_cosine_similarity_orthogonal_vectors(self):
        """Orthogonal vectors should have similarity 0.0."""
        result = cosine_similarity([1.0, 0.0], [0.0, 1.0])
        assert result == 0.0

    def test_cosine_similarity_opposite_vectors(self):
        """Opposite vectors should have similarity -1.0."""
        result = cosine_similarity([1.0, 0.0], [-1.0, 0.0])
        assert result == -1.0

    def test_cosine_similarity_handles_nan(self):
        """Should handle edge cases that could produce NaN."""
        # Very large values that could overflow
        result = cosine_similarity([1e300, 1e300], [1e300, 1e300])
        assert -1.0 <= result <= 1.0

    def test_parse_candidate_patterns(self):
        """Should parse candidate patterns from CoT output."""
        cot = """<candidate_patterns>
## CANDIDATE: Type Hints
Pattern: Always use type hints
Source Projects: proj-a, proj-b, proj-c
Confidence: HIGH
Evidence Type: adopted_rule
Rationale: Convergent across 3 projects
Convergence: 3
</candidate_patterns>"""
        patterns = parse_candidate_patterns(cot)
        assert len(patterns) == 1
        assert patterns[0].pattern_name == "Type Hints"
        assert patterns[0].convergence_count == 3

    def test_parse_global_proposals(self):
        """Should parse global proposals from CoT output."""
        cot = """<synthesis>
## GLOBAL PROPOSAL: Type Hints
Action: ADD
Target: conventions.md
Content: Always use type hints in function signatures
Source Projects: proj-a, proj-b, proj-c
Evidence Sessions: s1, s2, s3
Rationale: Strong convergence
</synthesis>"""
        proposals = parse_global_proposals(cot)
        assert len(proposals) == 1
        assert proposals[0].title == "Type Hints"
        assert proposals[0].action == "ADD"
        assert len(proposals[0].source_projects) == 3

    def test_extract_triage_content(self):
        """Should extract triage section."""
        cot = "<triage>Important analysis</triage>"
        result = extract_triage_content(cot)
        assert result == "Important analysis"

    def test_extract_cross_ref_content(self):
        """Should extract cross_ref section."""
        cot = "<cross_ref>Verification step</cross_ref>"
        result = extract_cross_ref_content(cot)
        assert result == "Verification step"

    def test_extract_synthesis_content(self):
        """Should extract synthesis section."""
        cot = "<synthesis>Final proposals</synthesis>"
        result = extract_synthesis_content(cot)
        assert result == "Final proposals"

    def test_assemble_global_phase1_prompt(self):
        """Should assemble Phase 1 prompt."""
        template = (
            "{global_rules}\n{project_rules}\n{project_traces}\n{embedding_clusters}"
        )
        result = assemble_global_phase1_prompt(
            global_rules="Global: be concise",
            project_rules_summaries=["Rules from proj-a", "Rules from proj-b"],
            project_traces_summaries=["Traces from proj-a"],
            embedding_clusters_summary="Clusters: type hints (3 projects)",
            prompt_template=template,
        )
        assert "Global: be concise" in result
        assert "Rules from proj-a" in result
        assert "Traces from proj-a" in result

    def test_assemble_global_phase2_prompt(self):
        """Should assemble Phase 2 prompt."""
        template = "{candidate_patterns}\n{global_rules}\n{project_evidence}"
        candidate = GlobalCandidatePattern(
            pattern_name="Type Hints",
            description="Use type hints",
            source_projects=["proj-a"],
            confidence="HIGH",
            evidence_type="rule",
            rationale="Convergence",
            convergence_count=3,
        )
        result = assemble_global_phase2_prompt(
            candidate_patterns=[candidate],
            global_rules="Global rules",
            evidence_summaries=["Evidence from proj-a"],
            prompt_template=template,
        )
        assert "CANDIDATE: Type Hints" in result
        assert "Global rules" in result


class TestGlobalEvolutionShellFunctions:
    """Tests for shell global evolution functions."""

    def test_read_global_rules_existing(self):
        """Should read global rules from valid directory."""
        with tempfile.TemporaryDirectory() as tmpdir:
            global_dir = Path(tmpdir)
            rules_dir = global_dir / "rules"
            rules_dir.mkdir(parents=True)
            (rules_dir / "conventions.md").write_text(
                "# Conventions\nAlways use type hints."
            )

            result = read_global_rules(global_dir)
            assert isinstance(result, MagicMock) or "Always use type hints" in str(
                result
            )

    def test_read_global_rules_missing(self):
        """Should return empty placeholder if no rules directory."""
        with tempfile.TemporaryDirectory() as tmpdir:
            result = read_global_rules(Path(tmpdir))
            # Returns Success with placeholder message when no rules directory
            from returns.result import Success

            assert isinstance(result, Success)
            assert "(No global rules)" in result.unwrap()

    def test_mark_rule_promoted(self):
        """Should mark a rule file as promoted."""
        with tempfile.TemporaryDirectory() as tmpdir:
            project_path = Path(tmpdir)
            lattice_dir = project_path / ".lattice" / "rules"
            lattice_dir.mkdir(parents=True)
            rule_file = lattice_dir / "test.md"
            rule_file.write_text("# Test Rule\nSome content.")

            result = mark_rule_promoted(project_path, "rules/test.md")
            assert result  # Should success

            content = rule_file.read_text()
            assert "<!-- lattice:promoted -->" in content

    def test_mark_rule_promoted_already_marked(self):
        """Should return success if already marked."""
        with tempfile.TemporaryDirectory() as tmpdir:
            project_path = Path(tmpdir)
            lattice_dir = project_path / ".lattice" / "rules"
            lattice_dir.mkdir(parents=True)
            rule_file = lattice_dir / "test.md"
            rule_file.write_text("<!-- lattice:promoted -->\n# Test Rule")

            result = mark_rule_promoted(project_path, "rules/test.md")
            # Should return True (already marked)
            assert result

    def test_mark_rule_promoted_missing_file(self):
        """Should return failure for missing file."""
        with tempfile.TemporaryDirectory() as tmpdir:
            result = mark_rule_promoted(Path(tmpdir), "rules/nonexistent.md")
            # Should return Failure
            assert isinstance(result, Failure)

    def test_mark_rule_promoted_resolves_path(self):
        """Should resolve rule file path correctly."""
        with tempfile.TemporaryDirectory() as tmpdir:
            project_path = Path(tmpdir)
            # Test with .lattice/rules/ structure
            lattice_dir = project_path / ".lattice" / "rules"
            lattice_dir.mkdir(parents=True)
            rule_file = lattice_dir / "conventions.md"
            rule_file.write_text("# Conventions\nTest rule.")

            # Should find file with rules/ prefix
            result = mark_rule_promoted(project_path, "rules/conventions.md")
            assert result
            assert "<!-- lattice:promoted -->" in rule_file.read_text()

    def test_write_evidence_to_global_db(self):
        """Should write evidence records to global.db and return rowids."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = Path(tmpdir) / "global.db"

            records = [
                EvidenceRecord(
                    source_project="proj-a",
                    source_session_id="s1",
                    source_rule_file="conv.md",
                    pattern="Type Hints",
                    summary="Use type hints",
                    original_snippet="def foo(x: int) -> str:",
                    confidence=0.9,
                )
            ]

            result = write_evidence_to_global_db(records, db_path)
            # Should return list of rowids
            assert isinstance(result, Success)
            rowids = result.unwrap()
            assert isinstance(rowids, list)
            assert len(rowids) == 1
            assert all(isinstance(rid, int) for rid in rowids)
            assert all(rid > 0 for rid in rowids)

    def test_write_rule_evidence_associations(self):
        """Should write rule_evidence associations."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = Path(tmpdir) / "global.db"
            # Create the database first
            store_result = create_global_store(db_path)
            assert isinstance(store_result, Success)
            store_result.unwrap().close()

            result = write_rule_evidence_associations("conv.md", [1, 2, 3], db_path)
            # Should succeed
            assert isinstance(result, Success)
            assert result.unwrap() == 3

    def test_write_evidence_embedding_validates_dimension(self):
        """Should reject embeddings that are not 768 dimensions."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = Path(tmpdir) / "global.db"
            # Create the database first
            store_result = create_global_store(db_path)
            assert isinstance(store_result, Success)
            store_result.unwrap().close()

            # Test with wrong dimension (128 instead of 768)
            wrong_dim_embedding = [0.1] * 128
            result = write_evidence_embedding(1, wrong_dim_embedding, db_path)
            # Should return Failure
            assert isinstance(result, Failure)
            assert "Expected 768-dim embedding" in result.failure()
            assert "got 128" in result.failure()

    def test_write_evidence_embedding_accepts_768_dimensions(self):
        """Should accept embeddings that are exactly 768 dimensions."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = Path(tmpdir) / "global.db"
            # Create the database first and add an evidence record
            from lattice.shell.global_evolution import (
                EvidenceRecord,
                write_evidence_to_global_db,
            )
            from returns.result import Success

            # First insert an evidence record to get a valid rowid
            records = [
                EvidenceRecord(
                    source_project="test",
                    source_session_id="s1",
                    source_rule_file="test.md",
                    pattern="Test",
                    summary="Test summary",
                    original_snippet="test",
                    confidence=0.9,
                )
            ]
            insert_result = write_evidence_to_global_db(records, db_path)
            assert isinstance(insert_result, Success)
            evidence_id = insert_result.unwrap()[0]

            # Now write a valid 768-dim embedding
            valid_embedding = [0.1] * 768
            result = write_evidence_embedding(evidence_id, valid_embedding, db_path)
            # Should succeed
            assert isinstance(result, Success)


class TestEmbeddingClustering:
    """Tests for embedding clustering functionality."""

    def test_compute_embedding_clusters_no_config(self):
        """Should return empty list when no embedding config."""
        from lattice.shell.global_evolution import ProjectRules
        from returns.result import Success

        project_rules = [
            ProjectRules(
                project_name="proj-a",
                project_path="/proj-a",
                rules_content="Test content",
                rule_files=["test.md"],
                promoted_count=0,
            )
        ]
        result = compute_embedding_clusters(project_rules, None)
        assert isinstance(result, Success)
        assert result.unwrap() == []

    def test_compute_embedding_clusters_with_similar_rules(self):
        """Should cluster rules with similar content."""
        from lattice.shell.global_evolution import ProjectRules
        from returns.result import Success

        project_rules = [
            ProjectRules(
                project_name="proj-a",
                project_path="/proj-a",
                rules_content="Always use type hints in Python",
                rule_files=["test.md"],
                promoted_count=0,
            ),
            ProjectRules(
                project_name="proj-b",
                project_path="/proj-b",
                rules_content="Use type hints for all functions",
                rule_files=["test.md"],
                promoted_count=0,
            ),
        ]

        # Without embedding config, should return empty list
        result = compute_embedding_clusters(project_rules, None)
        assert isinstance(result, Success)
        assert result.unwrap() == []


class TestConcurrencyLock:
    """Tests for concurrency lock behavior."""

    def test_concurrency_lock_file_creation(self):
        """Should create and release lock file properly."""
        import fcntl
        import sys

        with tempfile.TemporaryDirectory() as tmpdir:
            lock_path = Path(tmpdir) / "evolve.lock"

            # Acquire lock
            lock_fd = open(lock_path, "w")
            fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
            lock_fd.write(f"{sys.argv[0]}\n")
            lock_fd.flush()

            assert lock_path.exists()

            # Second lock should fail
            try:
                lock_fd2 = open(lock_path, "w")
                fcntl.flock(lock_fd2.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
                lock_fd2.write("another process\n")
                lock_fd2.flush()
                assert False, "Should have raised BlockingIOError"
            except BlockingIOError:
                pass  # Expected

            # Release lock
            fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN)
            lock_fd.close()

            # Now should be able to acquire
            lock_fd3 = open(lock_path, "w")
            fcntl.flock(lock_fd3.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
            fcntl.flock(lock_fd3.fileno(), fcntl.LOCK_UN)
            lock_fd3.close()


class TestPromotedRuleExclusion:
    """Tests for promoted rule exclusion."""

    def test_scan_project_rules_excludes_promoted(self):
        """Should exclude rules marked with promoted tag."""
        with tempfile.TemporaryDirectory() as tmpdir:
            project_path = Path(tmpdir)
            lattice_dir = project_path / ".lattice" / "rules"
            lattice_dir.mkdir(parents=True)

            # Create promoted rule
            promoted_rule = lattice_dir / "promoted.md"
            promoted_rule.write_text(
                "<!-- lattice:promoted -->\n# Promoted Rule\nContent."
            )

            # Create non-promoted rule
            regular_rule = lattice_dir / "regular.md"
            regular_rule.write_text("# Regular Rule\nContent.")

            result = scan_project_rules(
                [
                    ProjectRegistration(
                        name="test",
                        path=str(project_path),
                        registered_at="2026-01-01T00:00:00Z",
                    )
                ]
            )

            # Should succeed - promoted rules are excluded from content but counted
            assert isinstance(result, Success)
            rules_list = result.unwrap()
            # Verify promoted rule content is excluded
            assert not any("Promoted Rule" in r.rules_content for r in rules_list)
            # Verify regular rule content is included
            assert any("Regular Rule" in r.rules_content for r in rules_list)
            # Verify promoted_count reflects exclusion
            assert any(r.promoted_count > 0 for r in rules_list)


class TestQueryProjectEvidence:
    """Tests for evidence query functionality."""

    def test_query_project_evidence_missing_store(self):
        """Should return message when store.db doesn't exist."""
        with tempfile.TemporaryDirectory() as tmpdir:
            project_path = Path(tmpdir)
            result = query_project_evidence(project_path, "type hints")
            # Should return Success with message about no store
            assert isinstance(result, Success)
            evidence = result.unwrap()
            assert "No" in evidence and "store" in evidence.lower()

    def test_query_project_evidence_with_store(self):
        """Should query evidence from existing store."""
        with tempfile.TemporaryDirectory() as tmpdir:
            project_path = Path(tmpdir)
            lattice_dir = project_path / ".lattice"
            lattice_dir.mkdir(parents=True)

            # Create empty store
            from lattice.shell.schema import create_store

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

            result = query_project_evidence(project_path, "type hints")
            # Should return Success with message about no matching evidence
            assert isinstance(result, Success)


class TestGlobalPhaseIntegration:
    """Integration tests for global phase functions."""

    def test_global_phase1_no_config(self):
        """Should fail without LLM config."""
        result = run_global_phase1(
            llm_config=None,  # type: ignore[arg-type]
        )
        # Should fail - no LLM config
        assert isinstance(result, Failure)
        assert "config" in result.failure().lower() or "llm" in result.failure().lower()

    def test_global_phase2_empty_candidates(self):
        """Should return Success with empty proposals for empty candidate list."""
        result = run_global_phase2(
            candidates=[],
            llm_config=MagicMock(model="test-model"),
            projects=[],
        )
        # Should succeed - empty candidates is a valid upstream state
        assert isinstance(result, Success)
        output = result.unwrap()
        assert output.proposals == []
        assert "No candidates" in output.cot_output


class TestGlobalEvolutionOutputFormatting:
    """Tests for global evolution output formatting functions."""

    def test_format_global_trace_content(self):
        """Should format trace content with both phases."""
        content = format_global_trace_content(
            phase1_cot="Phase 1 output here",
            phase2_cot="Phase 2 output here",
            projects_scanned=5,
        )
        assert "# Global Evolution Trace" in content
        assert "## Phase 1: Rule Scan" in content
        assert "## Phase 2: Evidence Verification" in content
        assert "Phase 1 output here" in content
        assert "Phase 2 output here" in content
        assert "**Projects Scanned**: 5" in content

    def test_format_global_trace_content_empty(self):
        """Should handle empty CoT outputs."""
        content = format_global_trace_content(
            phase1_cot="",
            phase2_cot="",
            projects_scanned=0,
        )
        assert "(No output)" in content
        assert "**Projects Scanned**: 0" in content

    def test_format_global_proposal_content(self):
        """Should format proposals content."""
        proposal = GlobalProposal(
            title="Type Hints",
            action="ADD",
            target="conventions.md",
            content="Always use type hints in function signatures.",
            source_projects=["proj-a", "proj-b", "proj-c"],
            evidence_sessions=["s1", "s2"],
            rationale="Strong convergence across 3 projects.",
        )
        content = format_global_proposal_content(
            proposals=[proposal],
            timestamp="2026-02-19T12:00:00Z",
        )
        assert "# Global Evolution Proposals" in content
        assert "## Proposal 1: Type Hints" in content
        assert "**Action**: ADD" in content
        assert "**Target**: conventions.md" in content
        assert "**Converged from**: 3 projects" in content
        assert "Always use type hints" in content
        assert "proj-a, proj-b, proj-c" in content
        assert "Strong convergence" in content

    def test_format_global_proposal_content_empty(self):
        """Should handle empty proposals list."""
        content = format_global_proposal_content(
            proposals=[],
            timestamp="2026-02-19T12:00:00Z",
        )
        assert "# Global Evolution Proposals" in content
        assert "## No Converged Proposals" in content
        assert "No proposals reached convergence threshold" in content
        assert "**Converged Proposals**: 0" in content


class TestWriteGlobalEvolutionOutput:
    """Tests for write_global_evolution_output shell function."""

    def test_write_global_evolution_output_creates_files(self):
        """Should create trace and proposal files in drift/ directory."""
        with tempfile.TemporaryDirectory() as tmpdir:
            global_path = Path(tmpdir)

            proposals = [
                GlobalProposal(
                    title="Test Rule",
                    action="ADD",
                    target="rules/test.md",
                    content="Test content",
                    source_projects=["p1", "p2", "p3"],
                    evidence_sessions=["s1"],
                    rationale="Test rationale",
                )
            ]

            result = write_global_evolution_output(
                phase1_cot="Phase 1 CoT output",
                phase2_cot="Phase 2 CoT output",
                proposals=proposals,
                projects_scanned=5,
                global_path=global_path,
            )

            from returns.result import Success

            assert isinstance(result, Success)
            proposal_path, trace_path = result.unwrap()

            # Check files exist
            assert Path(trace_path).exists()
            assert Path(proposal_path).exists()

            # Check trace content
            trace_content = Path(trace_path).read_text()
            assert "# Global Evolution Trace" in trace_content
            assert "Phase 1 CoT output" in trace_content
            assert "Phase 2 CoT output" in trace_content
            assert "**Projects Scanned**: 5" in trace_content

            # Check proposal content
            proposal_content = Path(proposal_path).read_text()
            assert "# Global Evolution Proposals" in proposal_content
            assert "Test Rule" in proposal_content

    def test_write_global_evolution_output_empty_proposals(self):
        """Should handle empty proposals list."""
        with tempfile.TemporaryDirectory() as tmpdir:
            global_path = Path(tmpdir)

            result = write_global_evolution_output(
                phase1_cot="Phase 1",
                phase2_cot="Phase 2",
                proposals=[],
                projects_scanned=3,
                global_path=global_path,
            )

            from returns.result import Success

            assert isinstance(result, Success)
            proposal_path, trace_path = result.unwrap()

            # Check proposal shows no converged
            proposal_content = Path(proposal_path).read_text()
            assert "No Converged Proposals" in proposal_content

    def test_write_global_evolution_output_creates_directories(self):
        """Should create drift/ directories if they don't exist."""
        with tempfile.TemporaryDirectory() as tmpdir:
            global_path = Path(tmpdir)

            # Verify directories don't exist
            assert not (global_path / "drift").exists()

            result = write_global_evolution_output(
                phase1_cot="P1",
                phase2_cot="P2",
                proposals=[],
                projects_scanned=1,
                global_path=global_path,
            )

            from returns.result import Success

            assert isinstance(result, Success)

            # Verify directories were created
            assert (global_path / "drift" / "traces").exists()
            assert (global_path / "drift" / "proposals").exists()
