"""Tests for HDBSCAN-based lesson consolidation (Phase 12.1a).

Tests clustering of lesson memories by embedding similarity, dry-run safety,
execute mode supersession, and parameter tuning.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from claude_engram.consolidation import ConsolidationReport, consolidate_lessons
from claude_engram.storage import MemoryStorage

# Skip entire module if scikit-learn is not installed
sklearn = pytest.importorskip("sklearn", minversion="1.3")


@pytest.fixture()
def storage(tmp_path: Path) -> MemoryStorage:
    """Create a MemoryStorage with a temp database."""
    db = tmp_path / "test.db"
    s = MemoryStorage(db_path=db)
    yield s
    s.close()


def _store_lessons(storage: MemoryStorage, contents: list[str]) -> list[int]:
    """Helper to store multiple lesson memories."""
    return [storage.store(content=c, category="lesson") for c in contents]


# ---------------------------------------------------------------------------
# Cluster detection
# ---------------------------------------------------------------------------


class TestClusterDetection:
    """HDBSCAN should group semantically similar lessons."""

    def test_similar_lessons_cluster_together(self, storage: MemoryStorage) -> None:
        """Near-duplicate lessons should form a cluster.

        HDBSCAN is conservative with small datasets — needs enough density
        to distinguish clusters from noise. We use min_cluster_size=2 and
        enough similar items to form a detectable dense region.
        """
        # Group A: very similar deployment lessons (5 items for density)
        similar = [
            "LESSON: Always use blue-green deployments for zero-downtime releases on AWS",
            "LESSON: Blue-green deployment strategy on AWS eliminates downtime during releases",
            "LESSON: For zero-downtime releases, blue-green deployment on AWS is the best approach",
            "LESSON: Blue-green deployment on AWS ensures zero downtime when releasing",
            "LESSON: Zero-downtime releases on AWS require blue-green deployment approach",
        ]
        # Group B: unrelated lessons
        unrelated = [
            "LESSON: SQLite WAL mode improves concurrent read performance significantly",
            "LESSON: Python dataclasses reduce boilerplate compared to plain __init__ methods",
            "LESSON: Git worktrees allow parallel development without stashing changes",
        ]
        _store_lessons(storage, similar + unrelated)

        report = consolidate_lessons(storage, min_cluster_size=2, min_samples=2, dry_run=True)

        assert isinstance(report, ConsolidationReport)
        assert report.total_lessons == 8
        assert report.dry_run is True
        # At least one cluster should form from the similar group
        assert report.clusters_found >= 1

    def test_no_clusters_with_dissimilar_lessons(self, storage: MemoryStorage) -> None:
        """Completely unrelated lessons should not cluster."""
        diverse = [
            "LESSON: Use pytest fixtures for test setup to avoid repetition",
            "LESSON: Kubernetes pod autoscaling requires resource limits to be set",
            "LESSON: React useEffect cleanup prevents memory leaks in components",
            "LESSON: PostgreSQL JSONB indexes speed up document-style queries",
            "LESSON: Nginx reverse proxy buffers protect upstream servers from slow clients",
        ]
        _store_lessons(storage, diverse)

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=True)

        # All should be noise (no clusters formed)
        assert report.clusters_found == 0
        assert report.noise_count == 5
        assert report.memories_consolidated == 0

    def test_too_few_lessons_for_clustering(self, storage: MemoryStorage) -> None:
        """Fewer lessons than min_cluster_size should return empty report."""
        _store_lessons(storage, ["LESSON: One lesson", "LESSON: Two lessons"])

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=True)

        assert report.total_lessons == 2
        assert report.clusters_found == 0
        assert report.memories_consolidated == 0
        assert report.noise_count == 2

    def test_empty_database(self, storage: MemoryStorage) -> None:
        """No lessons at all should return clean empty report."""
        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=True)

        assert report.total_lessons == 0
        assert report.clusters_found == 0
        assert report.memories_consolidated == 0


# ---------------------------------------------------------------------------
# Dry run safety
# ---------------------------------------------------------------------------


class TestDryRun:
    """Dry run must never modify data."""

    def test_dry_run_does_not_modify_data(self, storage: MemoryStorage) -> None:
        """After dry_run, all lessons should still be active."""
        ids = _store_lessons(
            storage,
            [
                "LESSON: Always use blue-green deployments for zero-downtime releases",
                "LESSON: Blue-green deployment eliminates downtime during releases",
                "LESSON: For zero-downtime releases blue-green deployment is best",
            ],
        )

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=True)
        assert report.dry_run is True

        # Verify all memories are still active by searching
        results = storage.search(
            query="blue-green deployment",
            category="lesson",
            limit=10,
        )
        active_ids = {r["id"] for r in results}
        for mid in ids:
            assert mid in active_ids, f"Memory #{mid} should still be active after dry_run"

    def test_dry_run_reports_token_savings(self, storage: MemoryStorage) -> None:
        """Dry run should report projected savings even without modifying data."""
        _store_lessons(
            storage,
            [
                "LESSON: Always use blue-green deployments for zero-downtime releases on AWS",
                "LESSON: Blue-green deployment on AWS eliminates downtime during releases",
                "LESSON: For zero-downtime releases, blue-green deployment on AWS is best",
            ],
        )

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=True)

        if report.clusters_found > 0:
            assert report.tokens_saved > 0, "Should report projected token savings"
            assert report.memories_consolidated > 0


# ---------------------------------------------------------------------------
# Execute mode
# ---------------------------------------------------------------------------


class TestExecuteMode:
    """Execute mode should actually supersede redundant memories."""

    def test_execute_supersedes_originals(self, storage: MemoryStorage) -> None:
        """After execute, superseded lessons should be inactive."""
        ids = _store_lessons(
            storage,
            [
                # Short version
                "LESSON: Blue-green deployments eliminate downtime",
                # Medium version
                "LESSON: Use blue-green deployments for zero-downtime releases on AWS",
                # Long version (should be picked as representative)
                (
                    "LESSON: Always use blue-green deployment strategy for zero-downtime "
                    "releases on AWS. This involves maintaining two identical production "
                    "environments and switching traffic between them during deployment."
                ),
            ],
        )

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=False)

        if report.clusters_found > 0:
            assert report.dry_run is False

            # The longest memory should be representative (kept active)
            cluster = report.clusters[0]
            representative_id = cluster.representative_id

            # Verify representative is the longest content's ID
            assert representative_id == ids[2], "Representative should be the longest lesson"

            # Verify superseded memories are inactive
            for superseded_id in cluster.superseded_ids:
                results = storage.search(
                    query="blue-green deployment",
                    category="lesson",
                    limit=20,
                    include_inactive=False,
                )
                active_ids = {r["id"] for r in results}
                assert superseded_id not in active_ids, (
                    f"Memory #{superseded_id} should be inactive after consolidation"
                )

    def test_execute_preserves_representative(self, storage: MemoryStorage) -> None:
        """The representative memory should remain searchable after consolidation."""
        _store_lessons(
            storage,
            [
                "LESSON: Blue-green deployments eliminate downtime during releases",
                "LESSON: Use blue-green deployment for zero-downtime release strategy",
                (
                    "LESSON: Blue-green deployment is the recommended strategy for "
                    "achieving zero-downtime releases in production environments"
                ),
            ],
        )

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=False)

        if report.clusters_found > 0:
            representative_id = report.clusters[0].representative_id

            # Representative should still be searchable
            results = storage.search(
                query="blue-green deployment",
                category="lesson",
                limit=10,
                include_inactive=False,
            )
            active_ids = {r["id"] for r in results}
            assert representative_id in active_ids


# ---------------------------------------------------------------------------
# Parameter tuning
# ---------------------------------------------------------------------------


class TestParameters:
    """min_cluster_size and min_samples should affect clustering behavior."""

    def test_min_cluster_size_affects_results(self, storage: MemoryStorage) -> None:
        """Higher min_cluster_size should produce fewer/larger clusters."""
        lessons = [
            "LESSON: Always use blue-green deployments for zero-downtime releases",
            "LESSON: Blue-green deployment eliminates downtime during releases",
            "LESSON: For zero-downtime releases blue-green deployment is best",
            "LESSON: Blue-green deployment on AWS is the recommended release strategy",
        ]
        _store_lessons(storage, lessons)

        report_small = consolidate_lessons(storage, min_cluster_size=2, dry_run=True)
        report_large = consolidate_lessons(storage, min_cluster_size=4, dry_run=True)

        # With min_cluster_size=2, should be easier to form clusters
        # With min_cluster_size=4, needs more dense grouping
        assert report_small.clusters_found >= report_large.clusters_found

    def test_only_active_lessons_considered(self, storage: MemoryStorage) -> None:
        """Already-superseded lessons should not be included in clustering."""
        ids = _store_lessons(
            storage,
            [
                "LESSON: Blue-green deployments eliminate downtime",
                "LESSON: Use blue-green deployment for zero-downtime releases",
                "LESSON: Blue-green deployment is best for zero-downtime releases",
            ],
        )

        # Supersede the first one manually
        storage.supersede_memory(ids[0], ids[2])

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=True)

        # Only 2 active lessons remain — not enough for min_cluster_size=3
        assert report.total_lessons == 2
        assert report.clusters_found == 0

    def test_non_lesson_categories_excluded(self, storage: MemoryStorage) -> None:
        """Only lesson-category memories should be considered."""
        # Store similar content across different categories
        storage.store(
            content="Blue-green deployments eliminate downtime during releases",
            category="lesson",
        )
        storage.store(
            content="Blue-green deployment is the recommended release strategy",
            category="decision",
        )
        storage.store(
            content="Blue-green deployment approach for zero-downtime releases",
            category="general",
        )

        report = consolidate_lessons(storage, min_cluster_size=2, dry_run=True)

        # Only 1 lesson — not enough for clustering
        assert report.total_lessons == 1
        assert report.clusters_found == 0


# ---------------------------------------------------------------------------
# Report structure
# ---------------------------------------------------------------------------


class TestReportStructure:
    """ConsolidationReport should have correct structure and data."""

    def test_report_fields_present(self, storage: MemoryStorage) -> None:
        """Report should have all expected fields."""
        _store_lessons(storage, ["LESSON: Test lesson"])

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=True)

        assert hasattr(report, "total_lessons")
        assert hasattr(report, "clusters_found")
        assert hasattr(report, "memories_consolidated")
        assert hasattr(report, "tokens_saved")
        assert hasattr(report, "noise_count")
        assert hasattr(report, "dry_run")
        assert hasattr(report, "clusters")
        assert isinstance(report.clusters, list)

    def test_cluster_info_fields(self, storage: MemoryStorage) -> None:
        """ClusterInfo should have all expected fields when clusters form."""
        _store_lessons(
            storage,
            [
                "LESSON: Always use blue-green deployments for zero-downtime releases",
                "LESSON: Blue-green deployment eliminates downtime during releases",
                "LESSON: For zero-downtime releases blue-green deployment is best",
            ],
        )

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=True)

        if report.clusters_found > 0:
            cluster = report.clusters[0]
            assert hasattr(cluster, "cluster_id")
            assert hasattr(cluster, "member_ids")
            assert hasattr(cluster, "member_previews")
            assert hasattr(cluster, "representative_id")
            assert hasattr(cluster, "representative_preview")
            assert hasattr(cluster, "superseded_ids")
            assert hasattr(cluster, "token_savings")
            assert len(cluster.member_ids) >= 3
            assert cluster.representative_id in cluster.member_ids
            assert cluster.representative_id not in cluster.superseded_ids


# ---------------------------------------------------------------------------
# Temporal integration (Phase 12.1)
# ---------------------------------------------------------------------------


class TestConsolidationTemporal:
    """After consolidation, superseded memories should have valid_to set."""

    def test_superseded_memories_get_valid_to(self, storage: MemoryStorage) -> None:
        """Consolidation calls supersede_memory, which now sets valid_to."""
        _store_lessons(
            storage,
            [
                "LESSON: Blue-green deployments eliminate downtime during releases",
                "LESSON: Use blue-green deployment for zero-downtime release strategy",
                (
                    "LESSON: Blue-green deployment is the recommended strategy for "
                    "achieving zero-downtime releases in production environments"
                ),
            ],
        )

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=False)

        if report.clusters_found > 0:
            conn = storage._get_conn()
            for superseded_id in report.clusters[0].superseded_ids:
                row = conn.execute(
                    "SELECT valid_to, is_active FROM memories WHERE id = ?",
                    (superseded_id,),
                ).fetchone()
                assert row["is_active"] == 0
                assert row["valid_to"] is not None, (
                    f"Superseded memory {superseded_id} should have valid_to set"
                )

    def test_representative_keeps_null_valid_to(self, storage: MemoryStorage) -> None:
        """The representative (kept) memory should still have valid_to = NULL."""
        _store_lessons(
            storage,
            [
                "LESSON: Blue-green deployments eliminate downtime during releases",
                "LESSON: Use blue-green deployment for zero-downtime release strategy",
                (
                    "LESSON: Blue-green deployment is the recommended strategy for "
                    "achieving zero-downtime releases in production environments"
                ),
            ],
        )

        report = consolidate_lessons(storage, min_cluster_size=3, dry_run=False)

        if report.clusters_found > 0:
            rep_id = report.clusters[0].representative_id
            conn = storage._get_conn()
            row = conn.execute(
                "SELECT valid_to, is_active FROM memories WHERE id = ?",
                (rep_id,),
            ).fetchone()
            assert row["is_active"] == 1
            assert row["valid_to"] is None
