"""
tests/track_b/test_story_9_06.py

Story 9.06: EpochKnowledgeWriter — Axiom Persistence (KG + Qdrant)

Black Box Tests (BB):
    BB1  write() with 3 axioms → KG file has 3 new JSONL entries
    BB2  write() with 3 axioms → Qdrant mock received 3 upsert calls with
         axiom_type="distilled" in each payload
    BB3  Qdrant raises on upsert → KG still written, qdrant_upserts=0

White Box Tests (WB):
    WB1  KG write uses open(..., "a") append mode, NOT overwrite ("w")
    WB2  Each axiom gets its own individual Qdrant upsert call (not batch)

Additional / edge-case tests (9 more tests = 14 total):
    EDGE1  WriteResult.kg_file_path equals writer's kg_path (absolute path)
    EDGE2  WriteResult.jsonl_entries == len(axioms)
    EDGE3  WriteResult.qdrant_upserts == len(axioms) on full success
    EDGE4  Empty axiom list → WriteResult(jsonl_entries=0, qdrant_upserts=0)
    EDGE5  KG entry contains correct epoch_id and date fields
    EDGE6  No qdrant_client → qdrant_upserts=0, KG still written
    EDGE7  embed_fn is called per axiom (not skipped when provided)
    EDGE8  WriteResult and its fields are importable from core.epoch package
    EDGE9  KG_FILE_PATH constant points to genesis_evolution_learnings.jsonl

ALL external I/O mocked. tempfile.NamedTemporaryFile used for KG file tests.
"""

from __future__ import annotations

import json
import sys
import tempfile
import os
from dataclasses import dataclass
from pathlib import Path
from unittest.mock import MagicMock, call, patch

import pytest

# ---------------------------------------------------------------------------
# Path bootstrap
# ---------------------------------------------------------------------------

GENESIS_ROOT = "/mnt/e/genesis-system"
if GENESIS_ROOT not in sys.path:
    sys.path.insert(0, GENESIS_ROOT)

# ---------------------------------------------------------------------------
# Imports under test
# ---------------------------------------------------------------------------

from core.epoch.epoch_knowledge_writer import (  # noqa: E402
    EpochKnowledgeWriter,
    WriteResult,
    KG_FILE_PATH,
    QDRANT_COLLECTION,
)
from core.epoch.axiom_distiller import Axiom  # noqa: E402

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_axiom(
    idx: int = 1,
    content: str = "Read before build",
    category: str = "operations",
    confidence: float = 0.85,
    source_saga_ids: list | None = None,
) -> Axiom:
    """Build a single test Axiom."""
    return Axiom(
        id=f"epoch_2026_02_25_{idx:03d}",
        content=content,
        category=category,
        confidence=confidence,
        source_saga_ids=source_saga_ids or [f"saga_{idx:03d}"],
    )


def _make_axiom_list(count: int = 3) -> list[Axiom]:
    """Build a list of *count* distinct Axiom objects."""
    return [
        _make_axiom(idx=i, content=f"Axiom content {i}", category="operations")
        for i in range(1, count + 1)
    ]


def _make_mock_qdrant() -> MagicMock:
    """Return a fresh MagicMock simulating a Qdrant client."""
    qd = MagicMock()
    qd.upsert.return_value = None  # upsert returns None in real Qdrant client
    return qd


# ===========================================================================
# BB Tests — Black Box
# ===========================================================================


def test_bb1_kg_file_has_3_new_entries():
    """BB1: write() with 3 axioms → KG file has exactly 3 new JSONL entries."""
    axioms = _make_axiom_list(3)

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=None, kg_path=tmp_path)
        result = writer.write(axioms, epoch_id="epoch_2026_02_25")

        # Count lines in the file
        with open(tmp_path, "r", encoding="utf-8") as fh:
            lines = [ln.strip() for ln in fh if ln.strip()]

        assert len(lines) == 3, f"Expected 3 JSONL entries, got {len(lines)}"
        assert result.jsonl_entries == 3

        # Each line must be valid JSON
        for line in lines:
            entry = json.loads(line)
            assert "id" in entry
            assert "content" in entry
            assert "category" in entry
            assert "epoch" in entry
            assert "date" in entry
    finally:
        os.unlink(tmp_path)


def test_bb2_qdrant_upserts_with_axiom_type_distilled():
    """BB2: 3 axioms → 3 Qdrant upsert calls, each payload has axiom_type='distilled'."""
    axioms = _make_axiom_list(3)
    qd = _make_mock_qdrant()

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=qd, kg_path=tmp_path)
        result = writer.write(axioms, epoch_id="epoch_2026_02_25")

        assert qd.upsert.call_count == 3, (
            f"Expected 3 Qdrant upsert calls, got {qd.upsert.call_count}"
        )
        assert result.qdrant_upserts == 3

        # Verify each call used collection_name="genesis_scars"
        # and each point payload has axiom_type="distilled"
        for upsert_call in qd.upsert.call_args_list:
            _, kwargs = upsert_call
            assert kwargs["collection_name"] == QDRANT_COLLECTION, (
                f"Expected collection_name='{QDRANT_COLLECTION}', "
                f"got '{kwargs['collection_name']}'"
            )
            points = kwargs["points"]
            assert len(points) == 1, "Expected 1 point per upsert call"
            payload = points[0]["payload"]
            assert payload.get("axiom_type") == "distilled", (
                f"Expected axiom_type='distilled', got {payload.get('axiom_type')!r}"
            )
    finally:
        os.unlink(tmp_path)


def test_bb3_qdrant_failure_kg_still_written():
    """BB3: Qdrant raises on every upsert → KG still written, qdrant_upserts=0."""
    axioms = _make_axiom_list(3)
    qd = _make_mock_qdrant()
    qd.upsert.side_effect = RuntimeError("Qdrant connection refused")

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=qd, kg_path=tmp_path)
        result = writer.write(axioms, epoch_id="epoch_2026_02_25")

        # KG must still have entries
        with open(tmp_path, "r", encoding="utf-8") as fh:
            lines = [ln.strip() for ln in fh if ln.strip()]

        assert len(lines) == 3, (
            f"KG should have 3 entries even after Qdrant failure, got {len(lines)}"
        )
        assert result.jsonl_entries == 3, (
            f"jsonl_entries should be 3, got {result.jsonl_entries}"
        )
        assert result.qdrant_upserts == 0, (
            f"qdrant_upserts should be 0 after Qdrant failure, got {result.qdrant_upserts}"
        )
    finally:
        os.unlink(tmp_path)


# ===========================================================================
# WB Tests — White Box
# ===========================================================================


def test_wb1_kg_write_uses_append_mode():
    """WB1: KG write uses open(..., 'a') — append mode, never 'w' overwrite."""
    axioms = _make_axiom_list(2)

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        # Pre-populate with 1 existing line so we can detect whether it is overwritten
        existing_entry = json.dumps({"id": "pre_existing", "content": "old entry"})
        tmp.write(existing_entry + "\n")
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=None, kg_path=tmp_path)
        writer.write(axioms, epoch_id="epoch_2026_02_25")

        with open(tmp_path, "r", encoding="utf-8") as fh:
            lines = [ln.strip() for ln in fh if ln.strip()]

        # File should now have 3 lines (1 pre-existing + 2 new)
        assert len(lines) == 3, (
            f"Expected 3 lines (1 pre-existing + 2 new), got {len(lines)}: {lines}"
        )
        # Pre-existing line must still be first
        first = json.loads(lines[0])
        assert first["id"] == "pre_existing", (
            f"Pre-existing line was overwritten. First line: {lines[0]!r}"
        )
    finally:
        os.unlink(tmp_path)


def test_wb2_each_axiom_gets_individual_qdrant_upsert():
    """WB2: Each axiom triggers its own upsert call (not a single batch call)."""
    axioms = _make_axiom_list(4)
    qd = _make_mock_qdrant()

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=qd, kg_path=tmp_path)
        writer.write(axioms, epoch_id="epoch_test")

        # Must be 4 separate calls — not 1 call with 4 points
        assert qd.upsert.call_count == 4, (
            f"Expected 4 individual upsert calls (one per axiom), "
            f"got {qd.upsert.call_count}"
        )

        # Each call must have exactly 1 point in the 'points' list
        for upsert_call in qd.upsert.call_args_list:
            _, kwargs = upsert_call
            assert len(kwargs["points"]) == 1, (
                f"Expected 1 point per upsert call, "
                f"got {len(kwargs['points'])}: {kwargs['points']}"
            )
    finally:
        os.unlink(tmp_path)


# ===========================================================================
# Additional / Edge-Case Tests
# ===========================================================================


def test_edge1_write_result_kg_file_path_is_writer_kg_path():
    """EDGE1: WriteResult.kg_file_path equals the writer's configured kg_path."""
    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=None, kg_path=tmp_path)
        result = writer.write(_make_axiom_list(1), epoch_id="epoch_test")

        assert result.kg_file_path == tmp_path, (
            f"Expected kg_file_path='{tmp_path}', got '{result.kg_file_path}'"
        )
    finally:
        os.unlink(tmp_path)


def test_edge2_jsonl_entries_equals_axiom_count():
    """EDGE2: WriteResult.jsonl_entries == len(axioms) for various counts."""
    for count in [1, 5, 10]:
        axioms = _make_axiom_list(count)

        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
        ) as tmp:
            tmp_path = tmp.name

        try:
            writer = EpochKnowledgeWriter(qdrant_client=None, kg_path=tmp_path)
            result = writer.write(axioms, epoch_id="epoch_test")
            assert result.jsonl_entries == count, (
                f"For count={count}: expected jsonl_entries={count}, "
                f"got {result.jsonl_entries}"
            )
        finally:
            os.unlink(tmp_path)


def test_edge3_qdrant_upserts_equals_axiom_count_on_success():
    """EDGE3: WriteResult.qdrant_upserts == len(axioms) when all upserts succeed."""
    axioms = _make_axiom_list(5)
    qd = _make_mock_qdrant()

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=qd, kg_path=tmp_path)
        result = writer.write(axioms, epoch_id="epoch_test")

        assert result.qdrant_upserts == 5, (
            f"Expected qdrant_upserts=5, got {result.qdrant_upserts}"
        )
    finally:
        os.unlink(tmp_path)


def test_edge4_empty_axiom_list_returns_zero_counts():
    """EDGE4: Empty axiom list → WriteResult with jsonl_entries=0, qdrant_upserts=0."""
    qd = _make_mock_qdrant()

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=qd, kg_path=tmp_path)
        result = writer.write([], epoch_id="epoch_test")

        assert result.jsonl_entries == 0
        assert result.qdrant_upserts == 0
        assert qd.upsert.call_count == 0
    finally:
        os.unlink(tmp_path)


def test_edge5_kg_entry_has_correct_epoch_and_date_fields():
    """EDGE5: Each KG JSONL entry contains the correct epoch_id and a date string."""
    from datetime import datetime, timezone

    axiom = _make_axiom(1, content="Test content", category="test")
    epoch_id = "epoch_2026_02_25"

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=None, kg_path=tmp_path)
        writer.write([axiom], epoch_id=epoch_id)

        with open(tmp_path, "r", encoding="utf-8") as fh:
            entry = json.loads(fh.readline().strip())

        assert entry["epoch"] == epoch_id, (
            f"Expected epoch='{epoch_id}', got '{entry['epoch']}'"
        )
        assert "date" in entry, "KG entry must have 'date' field"
        # date must be in YYYY-MM-DD format
        date_str = entry["date"]
        datetime.strptime(date_str, "%Y-%m-%d")  # raises if format is wrong
        assert entry["id"] == axiom.id
        assert entry["content"] == axiom.content
        assert entry["category"] == axiom.category
    finally:
        os.unlink(tmp_path)


def test_edge6_no_qdrant_client_kg_still_written():
    """EDGE6: qdrant_client=None → qdrant_upserts=0, KG written successfully."""
    axioms = _make_axiom_list(2)

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(qdrant_client=None, kg_path=tmp_path)
        result = writer.write(axioms, epoch_id="epoch_test")

        assert result.qdrant_upserts == 0
        assert result.jsonl_entries == 2

        with open(tmp_path, "r", encoding="utf-8") as fh:
            lines = [ln.strip() for ln in fh if ln.strip()]
        assert len(lines) == 2
    finally:
        os.unlink(tmp_path)


def test_edge7_embed_fn_called_per_axiom():
    """EDGE7: When embed_fn is provided, it is called once per axiom."""
    axioms = _make_axiom_list(3)
    qd = _make_mock_qdrant()
    embed_call_count = {"n": 0}

    def _mock_embed(text: str) -> list[float]:
        embed_call_count["n"] += 1
        return [0.1, 0.2, 0.3, 0.4]

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
    ) as tmp:
        tmp_path = tmp.name

    try:
        writer = EpochKnowledgeWriter(
            qdrant_client=qd, embed_fn=_mock_embed, kg_path=tmp_path
        )
        writer.write(axioms, epoch_id="epoch_test")

        assert embed_call_count["n"] == 3, (
            f"Expected embed_fn called 3 times, got {embed_call_count['n']}"
        )
        # Each Qdrant point must use the embedded vector
        for upsert_call in qd.upsert.call_args_list:
            _, kwargs = upsert_call
            vector = kwargs["points"][0]["vector"]
            assert vector == [0.1, 0.2, 0.3, 0.4], (
                f"Expected embed vector [0.1, 0.2, 0.3, 0.4], got {vector}"
            )
    finally:
        os.unlink(tmp_path)


def test_edge8_package_init_exports_write_result_and_kg_file_path():
    """EDGE8: core.epoch __init__ exports EpochKnowledgeWriter, WriteResult,
    KG_FILE_PATH, QDRANT_COLLECTION."""
    from core.epoch import (  # noqa: F401
        EpochKnowledgeWriter as EKW,
        WriteResult as WR,
        KG_FILE_PATH as KFP,
        QDRANT_COLLECTION as QC,
    )

    assert EKW is EpochKnowledgeWriter
    assert WR is WriteResult
    assert KFP == KG_FILE_PATH
    assert QC == QDRANT_COLLECTION


def test_edge9_kg_file_path_constant_points_to_correct_file():
    """EDGE9: KG_FILE_PATH constant points to genesis_evolution_learnings.jsonl."""
    assert KG_FILE_PATH.endswith("genesis_evolution_learnings.jsonl"), (
        f"KG_FILE_PATH should end with 'genesis_evolution_learnings.jsonl', "
        f"got: '{KG_FILE_PATH}'"
    )
    assert "KNOWLEDGE_GRAPH" in KG_FILE_PATH, (
        f"KG_FILE_PATH should be under KNOWLEDGE_GRAPH/, got: '{KG_FILE_PATH}'"
    )
    assert KG_FILE_PATH.startswith("/mnt/e/"), (
        f"KG_FILE_PATH must be on E: drive, got: '{KG_FILE_PATH}'"
    )


# ===========================================================================
# Standalone runner (for direct execution)
# ===========================================================================

if __name__ == "__main__":
    import traceback

    tests = [
        ("BB1: 3 axioms → 3 KG entries", test_bb1_kg_file_has_3_new_entries),
        ("BB2: 3 Qdrant upserts with axiom_type=distilled", test_bb2_qdrant_upserts_with_axiom_type_distilled),
        ("BB3: Qdrant failure → KG still written", test_bb3_qdrant_failure_kg_still_written),
        ("WB1: KG write uses append mode", test_wb1_kg_write_uses_append_mode),
        ("WB2: individual Qdrant upserts per axiom", test_wb2_each_axiom_gets_individual_qdrant_upsert),
        ("EDGE1: WriteResult.kg_file_path == writer.kg_path", test_edge1_write_result_kg_file_path_is_writer_kg_path),
        ("EDGE2: jsonl_entries == len(axioms)", test_edge2_jsonl_entries_equals_axiom_count),
        ("EDGE3: qdrant_upserts == len(axioms) on success", test_edge3_qdrant_upserts_equals_axiom_count_on_success),
        ("EDGE4: empty list → zero counts", test_edge4_empty_axiom_list_returns_zero_counts),
        ("EDGE5: KG entry has epoch + date fields", test_edge5_kg_entry_has_correct_epoch_and_date_fields),
        ("EDGE6: no qdrant_client → KG still written", test_edge6_no_qdrant_client_kg_still_written),
        ("EDGE7: embed_fn called per axiom", test_edge7_embed_fn_called_per_axiom),
        ("EDGE8: __init__ exports WriteResult + KG_FILE_PATH", test_edge8_package_init_exports_write_result_and_kg_file_path),
        ("EDGE9: KG_FILE_PATH constant is correct", test_edge9_kg_file_path_constant_points_to_correct_file),
    ]

    passed = 0
    total = len(tests)
    for name, fn in tests:
        try:
            fn()
            print(f"  [PASS] {name}")
            passed += 1
        except Exception as exc:  # noqa: BLE001
            print(f"  [FAIL] {name}: {exc}")
            traceback.print_exc()

    print(f"\n{passed}/{total} tests passed")
    if passed == total:
        print("ALL TESTS PASSED -- Story 9.06 (Track B)")
    else:
        sys.exit(1)
