"""Integration tests: cross-module build integration workflows.

Tests untested cross-module interactions covering:
- Quality engine duplicate detection → merge → re-validation cycle
- Extraction pipeline → knowledge creation → session tracking
- Document store → knowledge graph → context engine roundtrip
- Validator + Quality engine combined health check
- Dedup observations → quality re-assessment
- Document lifecycle with summarization and context budget
- Multi-module state consistency under concurrent operations
"""

import json
import sqlite3
from datetime import datetime, timedelta, timezone

import pytest

from greymatter_core.context import ContextEngine
from greymatter_core.db import SQLiteDatabase
from greymatter_core.documents import DocumentStore
from greymatter_core.extraction import ExtractionPipeline
from greymatter_core.knowledge import KnowledgeStore
from greymatter_core.patterns import PatternManager
from greymatter_core.quality import QualityEngine
from greymatter_core.sessions import SessionManager
from greymatter_core.standards import StandardsManager
from greymatter_core.validate import KnowledgeValidator
from greymatter_core.work_items import WorkItemManager


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

@pytest.fixture
def db():
    """Shared in-memory database for build integration tests."""
    database = SQLiteDatabase(":memory:")
    yield database
    database.close()


@pytest.fixture
def knowledge(db):
    return KnowledgeStore(db)


@pytest.fixture
def patterns(db):
    return PatternManager(db)


@pytest.fixture
def sessions(db):
    return SessionManager(db)


@pytest.fixture
def work_items(db):
    return WorkItemManager(db)


@pytest.fixture
def quality(db):
    return QualityEngine(db)


@pytest.fixture
def validator(db):
    return KnowledgeValidator(db)


@pytest.fixture
def doc_store(db):
    return DocumentStore(db)


@pytest.fixture
def standards(db):
    return StandardsManager(db)


@pytest.fixture
def context_engine(db, doc_store, standards, patterns):
    return ContextEngine(db, doc_store, standards, patterns)


# ---------------------------------------------------------------------------
# 1. Quality Duplicate Detection → Merge → Re-Validation
# ---------------------------------------------------------------------------

class TestQualityMergeCycle:
    """Tests the full quality cycle: detect duplicates → merge → verify cleanup."""

    def test_detect_merge_and_reverify(self, knowledge, quality):
        """Detect near-duplicates, merge them, then verify quality improves."""
        # Create two near-duplicate entries
        e1 = knowledge.create(
            entry_type="expertise",
            title="SQLite WAL mode configuration",
            content="Enable WAL mode for concurrent read access during writes",
            domain="database",
        )
        e2 = knowledge.create(
            entry_type="reference",
            title="SQLite WAL mode configuration guide",
            content="Enable WAL mode for concurrent read access during writes in SQLite",
            domain="database",
        )

        # Quality check should detect the near-duplicate pair
        report_before = quality.full_check()
        assert report_before.total_entries == 2

        dup_issues = [i for i in report_before.issues if i.issue_type == "duplicate"]
        assert len(dup_issues) >= 1
        score_before = report_before.score

        # Merge: keep the higher-trust entry (expertise > reference)
        merged = quality.merge_duplicates(e1["id"], e2["id"])
        assert merged is True

        # Re-check quality — duplicate should be resolved
        report_after = quality.full_check()
        assert report_after.total_entries == 1  # Only keeper remains active

        dup_issues_after = [i for i in report_after.issues if i.issue_type == "duplicate"]
        assert len(dup_issues_after) == 0
        assert report_after.score >= score_before

    def test_merge_appends_content_from_removed(self, knowledge, quality):
        """Merging duplicates appends the removed entry's unique content."""
        keeper = knowledge.create(
            entry_type="expertise",
            title="Token bucket algorithm",
            content="Token bucket is effective for rate limiting",
            domain="api",
        )
        duplicate = knowledge.create(
            entry_type="observation",
            title="Token bucket algorithm notes",
            content="Token bucket supports burst traffic via token accumulation",
            domain="api",
        )

        quality.merge_duplicates(keeper["id"], duplicate["id"])

        merged_entry = knowledge.get(keeper["id"])
        assert "burst traffic" in merged_entry["content"]
        assert "rate limiting" in merged_entry["content"]

    def test_merge_prunes_duplicate_entry(self, knowledge, quality):
        """The removed entry is pruned (not deleted) for audit trail."""
        e1 = knowledge.create(
            entry_type="correction",
            title="Auth token must be refreshed",
            content="Always refresh tokens before they expire",
            domain="auth",
        )
        e2 = knowledge.create(
            entry_type="observation",
            title="Auth token must be refreshed periodically",
            content="Refresh auth tokens to avoid session expiration",
            domain="auth",
        )

        quality.merge_duplicates(e1["id"], e2["id"])

        pruned = knowledge.get(e2["id"])
        assert pruned is not None
        assert pruned["pruned_at"] is not None

        # Pruned entry excluded from default search
        active = knowledge.search(domain="auth")
        active_ids = [e["id"] for e in active]
        assert e1["id"] in active_ids
        assert e2["id"] not in active_ids


# ---------------------------------------------------------------------------
# 2. Extraction Pipeline → Knowledge → Session Tracking
# ---------------------------------------------------------------------------

class TestExtractionKnowledgePipeline:
    """Tests extracting observations from text and tracking through sessions."""

    def test_heuristic_extraction_creates_observations(self, knowledge, sessions):
        """Extracted observations from text can be stored as knowledge entries."""
        session = sessions.start(project="greymatter")

        text = (
            "We decided to use SQLite with WAL mode for local persistence. "
            "I realized that connection pooling is unnecessary for single-user apps. "
            "Remember to always close database connections in finally blocks."
        )
        observations = ExtractionPipeline.extract_heuristic(
            text, session_id=session["id"], trigger="conversation",
        )
        assert len(observations) >= 2  # Should catch decision + insight + pattern

        # Store each extracted observation as a knowledge entry
        created_ids = []
        for obs in observations:
            entry = knowledge.create(
                entry_type="observation",
                title=obs["title"],
                content=obs["content"],
                domain="database",
            )
            created_ids.append(entry["id"])

        sessions.end(session["id"], summary="Extracted database observations")

        # Verify all observations persisted
        db_entries = knowledge.search(domain="database")
        assert len(db_entries) >= 2
        for eid in created_ids:
            assert knowledge.get(eid) is not None

    def test_extraction_classification_accuracy(self):
        """ExtractionPipeline correctly classifies different text categories."""
        decision_text = "We decided to use FastAPI for the REST layer."
        correction_text = "Actually, that's wrong — the endpoint should return 201, not 200."
        insight_text = "I realized the bottleneck is in the serialization step."
        pattern_text = "We always validate input at the API boundary."

        decisions = ExtractionPipeline.extract_heuristic(decision_text)
        corrections = ExtractionPipeline.extract_heuristic(correction_text)
        insights = ExtractionPipeline.extract_heuristic(insight_text)
        patterns = ExtractionPipeline.extract_heuristic(pattern_text)

        assert any(o["category"] == "decision" for o in decisions)
        assert any(o["category"] == "correction" for o in corrections)
        assert any(o["category"] == "insight" for o in insights)
        assert any(o["category"] == "pattern" for o in patterns)

    def test_extraction_to_pattern_promotion(self, knowledge, patterns, sessions):
        """Extracted observations can feed pattern creation and evidence."""
        text = "I noticed that agents consistently produce better results when given structured JSON schemas."
        observations = ExtractionPipeline.extract_heuristic(
            text, session_id="sess-extract-001",
        )
        assert len(observations) >= 1

        # Store as observation
        obs_entry = knowledge.create(
            entry_type="observation",
            title=observations[0]["title"],
            content=observations[0]["content"],
            domain="orchestration",
        )

        # Create a pattern from the observation
        pat = patterns.create(
            type="process",
            domain="orchestration",
            title="Structured output for agents",
            observation="Agents produce better results with JSON schemas",
        )

        # Accumulate evidence across sessions
        for _ in range(3):
            s = sessions.start(project="greymatter")
            patterns.add_evidence(pat["id"], session_id=s["id"])
            sessions.end(s["id"])

        confirmed = patterns.get(pat["id"])
        assert confirmed["confidence"] == "medium"
        assert confirmed["confirmations"] == 3


# ---------------------------------------------------------------------------
# 3. Document Store → Context Engine → Session Integration
# ---------------------------------------------------------------------------

class TestDocumentContextIntegration:
    """Tests document lifecycle integrated with context engine and sessions."""

    def test_document_creation_during_session_appears_in_context(
        self, doc_store, context_engine, sessions,
    ):
        """Documents created during a session appear in context for that project."""
        session = sessions.start(project="greymatter")

        doc_store.create(
            doc_type="design",
            title="Coordinator State Machine",
            content="The coordinator uses a finite state machine with states: idle, dispatching, monitoring.",
            project="greymatter",
        )
        doc_store.create(
            doc_type="research",
            title="Agent Dispatch Strategy",
            content="Dispatch uses round-robin with priority weighting for agent assignment.",
            project="greymatter",
        )

        sessions.end(session["id"], summary="Documented coordinator design")

        ctx = context_engine.build_session_context(
            project="greymatter", max_tokens=50000,
        )
        titles = [d["title"] for d in ctx["recent_context"]]
        assert "Coordinator State Machine" in titles
        assert "Agent Dispatch Strategy" in titles

    def test_document_summarization_respected_by_context(self, doc_store, context_engine):
        """Documents with summaries use summary when budget is tight."""
        large_content = "Detailed architecture description. " * 200
        doc = doc_store.create(
            doc_type="design",
            title="Architecture Overview",
            content=large_content,
            project="greymatter",
        )

        doc_store.summarize(doc["id"], "High-level architecture for GreyMatter Solo.")

        # With tight budget, context engine should serve summary
        ctx = context_engine.build_session_context(
            project="greymatter", max_tokens=100,
        )
        total_tokens = sum(
            d.get("token_estimate", 0) or 0 for d in ctx["recent_context"]
        )
        assert total_tokens <= 100

    def test_document_count_and_search_consistency(self, doc_store):
        """Document count matches search results across types and projects."""
        doc_store.create(
            doc_type="design", title="Design A",
            content="Content A", project="greymatter",
        )
        doc_store.create(
            doc_type="research", title="Research B",
            content="Content B", project="greymatter",
        )
        doc_store.create(
            doc_type="design", title="Design C",
            content="Content C", project="salesos",
        )

        assert doc_store.count() == 3
        assert doc_store.count(project="greymatter") == 2
        assert doc_store.count(doc_type="design") == 2
        assert doc_store.count(doc_type="design", project="greymatter") == 1

        gm_docs = doc_store.search(project="greymatter")
        assert len(gm_docs) == 2

    def test_search_summary_returns_compact_results(self, doc_store):
        """search_summary returns metadata without full content."""
        doc_store.create(
            doc_type="design",
            title="Detailed Design",
            content="A very long and detailed document. " * 100,
            project="greymatter",
        )

        summary = doc_store.search_summary(project="greymatter")
        assert summary["total"] == 1
        assert summary["returned"] == 1
        doc = summary["documents"][0]
        assert "content_preview" in doc
        # Preview should be truncated, not full content
        assert len(doc["content_preview"]) <= 200


# ---------------------------------------------------------------------------
# 4. Validator + Quality Engine Combined Health Check
# ---------------------------------------------------------------------------

class TestValidatorQualityCombined:
    """Tests running both validator and quality engine for comprehensive health."""

    def test_combined_health_check_workflow(self, db, knowledge, quality, validator):
        """Run both quality and validation checks for a complete health report."""
        # Create entries with varying quality
        knowledge.create(
            entry_type="expertise",
            title="Async patterns in Python",
            content="Use asyncio.TaskGroup for structured concurrency",
            domain="orchestration",
        )
        knowledge.create(
            entry_type="gotcha",
            title="Never block the event loop",
            content="Blocking calls in async code cause cascading timeouts",
            domain="orchestration",
        )

        # Run quality check
        quality_report = quality.full_check()
        assert quality_report.total_entries == 2
        assert quality_report.score > 0

        # Run validation check
        validation_report = validator.validate_all()
        # These entries don't reference file paths, so path validation passes
        health = {
            "quality_score": quality_report.score,
            "quality_issues": quality_report.issue_count,
            "validation_valid": validation_report.valid,
            "validation_invalid": validation_report.invalid,
            "validation_stale": validation_report.stale,
        }
        assert health["quality_score"] > 0
        assert health["validation_invalid"] == 0

    def test_stale_entries_detected_by_both_engines(self, db, knowledge, quality, validator):
        """Stale entries are flagged by validator when old and low-confidence."""
        entry = knowledge.create(
            entry_type="observation",
            title="Temporary workaround for auth bug",
            content="Using retry loop as temporary fix",
            domain="auth",
        )

        # Backdate the entry and set low confidence to trigger staleness
        db.execute(
            """UPDATE knowledge_entries
               SET created_at = datetime('now', '-120 days'),
                   updated_at = datetime('now', '-120 days'),
                   confidence = 0.3
               WHERE id = ?""",
            (entry["id"],),
        )

        validation_report = validator.validate_stale(days=30)
        assert validation_report.stale >= 1

        stale_issues = [
            i for i in validation_report.issues
            if "Temporary workaround" in i.title
        ]
        assert len(stale_issues) == 1

    def test_validator_weekly_run_tracking(self, db, validator):
        """Validator records run timestamps and respects weekly cadence."""
        # First run should always be due
        assert validator.should_run_weekly() is True

        # Record a run
        validator.record_run()

        # Should not be due immediately after
        assert validator.should_run_weekly() is False

    def test_quality_contradiction_detection(self, knowledge, quality):
        """Quality engine detects contradicting entries in the same domain."""
        # Entries must share significant topic overlap AND have negation pairs
        knowledge.create(
            entry_type="expertise",
            title="Always enable database connection pooling",
            content="You should always enable connection pooling for database access to improve performance",
            domain="database",
        )
        knowledge.create(
            entry_type="correction",
            title="Never enable database connection pooling",
            content="You should never enable connection pooling for database access in single-user SQLite",
            domain="database",
        )

        contradictions = quality.find_contradictions()
        assert len(contradictions) >= 1
        assert contradictions[0].issue_type == "contradiction"
        assert contradictions[0].severity == "high"


# ---------------------------------------------------------------------------
# 5. Extraction → Dedup-Aware Knowledge Pipeline
# ---------------------------------------------------------------------------

class TestExtractionDedupPipeline:
    """Tests extraction creating observations that respect dedup constraints."""

    def test_duplicate_extractions_detected_by_quality(self, knowledge, quality):
        """Multiple extractions of similar text produce detectable duplicates."""
        # Simulate extracting the same insight from two conversations
        for i in range(2):
            knowledge.create(
                entry_type="observation",
                title=f"Always validate JWT expiry claims (session {i})",
                content="Check the exp claim in JWT tokens before trusting the payload",
                domain="security",
            )

        report = quality.full_check()
        dup_issues = [i for i in report.issues if i.issue_type == "duplicate"]
        assert len(dup_issues) >= 1

    def test_extraction_with_quality_gate(self, knowledge, quality, sessions):
        """Full flow: extract → store → quality check → identify issues."""
        session = sessions.start(project="greymatter")

        text = (
            "We decided to use JWT for authentication. "
            "Best practice is to always validate the token signature. "
            "I realized that token rotation is critical for security."
        )
        observations = ExtractionPipeline.extract_heuristic(
            text, session_id=session["id"],
        )

        for obs in observations:
            knowledge.create(
                entry_type="observation",
                title=obs["title"],
                content=obs["content"],
                domain="security",
            )

        sessions.end(session["id"])

        report = quality.full_check()
        assert report.total_entries >= len(observations)
        # Quality score should be reasonable for fresh entries
        assert report.score > 0.5


# ---------------------------------------------------------------------------
# 6. Multi-Module State Consistency
# ---------------------------------------------------------------------------

class TestMultiModuleConsistency:
    """Tests state consistency when multiple modules operate on shared DB."""

    def test_session_work_pattern_knowledge_roundtrip(
        self, sessions, work_items, patterns, knowledge, quality,
    ):
        """Full agent workflow: session → work → pattern → knowledge → quality."""
        # Start session and claim work
        session = sessions.start(project="greymatter")
        work = work_items.create(
            title="Implement circuit breaker",
            priority=10,
            project="greymatter",
            feature="resilience",
        )
        work_items.update_status(work["id"], "in_progress")

        # Discover a pattern during work
        pat = patterns.create(
            type="process",
            domain="resilience",
            title="Circuit breaker for external calls",
            observation="Wrap HTTP calls in circuit breaker to prevent cascading failures",
        )
        patterns.add_evidence(
            pat["id"],
            session_id=session["id"],
            work_item_id=work["id"],
        )

        # Capture knowledge
        kn = knowledge.create(
            entry_type="expertise",
            title="Circuit breaker implementation",
            content="Use open/half-open/closed states with configurable thresholds",
            domain="resilience",
            tags="circuit-breaker,resilience",
        )

        # Quality gate
        report = quality.full_check()
        assert report.total_entries >= 1
        assert report.score > 0.5

        # Complete work
        work_items.update_status(work["id"], "pending_review")
        work_items.update_review(work["id"], "approved", "LGTM")
        work_items.complete(work["id"])

        sessions.end(session["id"], summary="Implemented circuit breaker pattern")

        # Verify final state
        ended = sessions.get(session["id"])
        assert ended["ended_at"] is not None

        completed = work_items.get(work["id"])
        assert completed["status"] == "completed"

        assert knowledge.get(kn["id"]) is not None
        assert patterns.get(pat["id"])["applications"] == 1

    def test_knowledge_quality_validator_all_consistent(
        self, knowledge, quality, validator,
    ):
        """Knowledge, quality, and validator all operate on the same data set."""
        entries = []
        for i in range(5):
            e = knowledge.create(
                entry_type="expertise",
                title=f"Best practice #{i}: unique topic area {i}",
                content=f"Detailed explanation of best practice number {i} in area {i}",
                domain=f"domain-{i}",
            )
            entries.append(e)

        # All modules see the same count
        assert knowledge.count() == 5
        quality_report = quality.full_check()
        assert quality_report.total_entries == 5

        # Prune one entry
        knowledge.prune(entries[2]["id"])
        assert knowledge.count() == 4

        quality_report_after = quality.full_check()
        assert quality_report_after.total_entries == 4

    def test_document_knowledge_context_pipeline(
        self, doc_store, knowledge, context_engine, standards, sessions,
    ):
        """Documents and knowledge entries both feed into context assembly."""
        standards.seed_defaults()

        session = sessions.start(project="greymatter")

        # Create a document
        doc_store.create(
            doc_type="design",
            title="API Gateway Design",
            content="The API gateway handles routing, rate limiting, and auth.",
            project="greymatter",
        )

        # Create related knowledge
        knowledge.create(
            entry_type="expertise",
            title="Gateway rate limiting strategy",
            content="Use token bucket with per-client quotas",
            domain="api",
        )

        sessions.end(session["id"])

        # Context should include both documents and standards
        ctx = context_engine.build_session_context(
            project="greymatter", max_tokens=50000,
        )
        assert ctx["recent_context"]  # Documents present
        assert json.loads(ctx["quality_standards"])  # Standards present

        doc_titles = [d["title"] for d in ctx["recent_context"]]
        assert "API Gateway Design" in doc_titles

    def test_trust_weights_affect_merge_direction(self, knowledge, quality):
        """When merging duplicates, higher trust entry is kept as primary."""
        # correction (1.0) > observation (0.4)
        correction = knowledge.create(
            entry_type="correction",
            title="Database timeout configuration",
            content="Set database timeout to 30 seconds, not the default 5",
            domain="database",
        )
        observation = knowledge.create(
            entry_type="observation",
            title="Database timeout configuration notes",
            content="Database timeout should be configured to 30 seconds",
            domain="database",
        )

        # Verify trust weights
        assert quality.get_trust_weight("correction") > quality.get_trust_weight("observation")

        # Merge: correction is higher trust, should be kept
        merged = quality.merge_duplicates(correction["id"], observation["id"])
        assert merged is True

        # Correction is still active, observation is pruned
        assert knowledge.get(correction["id"])["pruned_at"] is None
        assert knowledge.get(observation["id"])["pruned_at"] is not None


# ---------------------------------------------------------------------------
# 7. Document Import and Export Workflow
# ---------------------------------------------------------------------------

class TestDocumentImportExport:
    """Tests document file import and markdown export workflows."""

    def test_import_file_and_export_markdown(self, doc_store, tmp_path):
        """Import a file, then export it as markdown with metadata header."""
        # Create a temporary markdown file
        md_file = tmp_path / "test_design.md"
        md_file.write_text("# Design\n\nThis is the system architecture.")

        doc = doc_store.import_file(
            file_path=str(md_file),
            doc_type="design",
            project="greymatter",
            tags="architecture,design",
        )
        assert doc["title"] == "test_design"
        assert doc["doc_type"] == "design"
        assert "system architecture" in doc["content"]

        # Export as markdown
        exported = doc_store.export_markdown(doc["id"])
        assert "# test_design" in exported
        assert "**Type:** design" in exported
        assert "**Project:** greymatter" in exported
        assert "system architecture" in exported

    def test_document_parent_child_relationship(self, doc_store):
        """Documents can reference parent documents for hierarchical structure."""
        parent = doc_store.create(
            doc_type="design",
            title="System Architecture",
            content="Top-level system design",
            project="greymatter",
        )
        child = doc_store.create(
            doc_type="design",
            title="Database Layer Design",
            content="SQLite schema and access patterns",
            project="greymatter",
            parent_id=parent["id"],
        )

        retrieved_child = doc_store.get(child["id"])
        assert retrieved_child["parent_id"] == parent["id"]

        # Both exist independently
        assert doc_store.count(project="greymatter") == 2


# ---------------------------------------------------------------------------
# 8. Context Engine Token Budget Under Pressure
# ---------------------------------------------------------------------------

class TestContextBudgetPressure:
    """Tests context engine behavior under various token budget constraints."""

    def test_zero_budget_returns_empty_context(self, context_engine):
        """With zero token budget, context returns empty but valid structure."""
        ctx = context_engine.build_session_context(max_tokens=0)
        assert "quality_standards" in ctx
        assert "recent_context" in ctx
        assert "active_patterns" in ctx

    def test_budget_distributes_across_sources(
        self, standards, doc_store, patterns, sessions, context_engine,
    ):
        """Token budget is shared across standards, documents, and patterns."""
        standards.seed_defaults()

        # Create multiple documents of varying size
        for i in range(5):
            doc_store.create(
                doc_type="design",
                title=f"Design Doc {i}",
                content=f"Content for design document number {i}. " * (10 * (i + 1)),
                project="greymatter",
            )

        # Create and escalate a pattern
        pat = patterns.create(
            type="process", domain="testing",
            title="Integration test coverage",
            observation="Every cross-module interaction needs an integration test",
        )
        for _ in range(3):
            s = sessions.start(project="greymatter")
            patterns.add_evidence(pat["id"], session_id=s["id"])
            sessions.end(s["id"])

        # With generous budget, we get everything
        ctx_full = context_engine.build_session_context(
            project="greymatter", max_tokens=50000,
        )
        assert len(ctx_full["recent_context"]) > 0
        assert json.loads(ctx_full["quality_standards"])

        # With tight budget, documents are constrained
        ctx_tight = context_engine.build_session_context(
            project="greymatter", max_tokens=500,
        )
        total_tokens = sum(
            d.get("token_estimate", 0) or 0 for d in ctx_tight["recent_context"]
        )
        assert total_tokens <= 500
