"""
tests/test_export_import.py — Tests for cortex.tools.export_import

Covers:
    - JSON export/import roundtrip
    - CSV export format (headers, content)
    - Markdown export (categories grouped, headers present)
    - Mem0 JSONL import mapping
    - dry_run mode (no writes)
    - Dedup on import (no double-import)
    - ImportResult dataclass
    - _infer_subject_predicate heuristic
    - Error handling (bad JSON, missing fields)
"""
from __future__ import annotations

import csv
import json
import os
import tempfile
from pathlib import Path

import pytest
import pytest_asyncio

# ---------------------------------------------------------------------------
# Fixtures — in-memory SQLite storage
# ---------------------------------------------------------------------------

@pytest_asyncio.fixture
async def storage(tmp_path):
    """Create a fresh SQLiteAdapter backed by a temp file."""
    from cortex.storage.sqlite_adapter import SQLiteAdapter
    db = tmp_path / "test_cortex.db"
    adapter = SQLiteAdapter(db_path=str(db))
    await adapter.initialize()
    yield adapter
    await adapter.close()


async def _seed_claims(storage, owner_id: str = "eva"):
    """Insert a handful of test claims."""
    await storage.create_claim(
        subject="Andrew",
        predicate="prefers",
        object_value="dark mode",
        owner_id=owner_id,
        confidence=0.9,
        category="preferences",
        memory_type="long-term",
    )
    await storage.create_claim(
        subject="Eva",
        predicate="is",
        object_value="an AI assistant",
        owner_id=owner_id,
        confidence=1.0,
        category="identity",
        memory_type="long-term",
    )
    await storage.create_claim(
        subject="100Yen",
        predicate="builds",
        object_value="affordable AI apps",
        owner_id=owner_id,
        confidence=0.8,
        category="projects",
        memory_type="long-term",
    )


# ---------------------------------------------------------------------------
# 1. JSON export produces valid structure
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_json_export_structure(storage, tmp_path):
    await _seed_claims(storage)
    out = str(tmp_path / "export.json")

    from cortex.tools.export_import import export_json
    count = await export_json(storage, owner_id="eva", output_path=out)

    assert count == 3
    data = json.loads(Path(out).read_text())
    assert data["version"] == "1.0"
    assert data["owner_id"] == "eva"
    assert "exported_at" in data
    assert isinstance(data["claims"], list)
    assert isinstance(data["entities"], list)
    assert isinstance(data["cornerstones"], list)
    assert len(data["claims"]) == 3


# ---------------------------------------------------------------------------
# 2. JSON export contains correct fields per claim
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_json_export_claim_fields(storage, tmp_path):
    await _seed_claims(storage)
    out = str(tmp_path / "export.json")

    from cortex.tools.export_import import export_json
    await export_json(storage, owner_id="eva", output_path=out)

    data = json.loads(Path(out).read_text())
    claim = data["claims"][0]
    for field in ("id", "subject", "predicate", "object_value", "confidence", "status"):
        assert field in claim, f"Missing field: {field}"


# ---------------------------------------------------------------------------
# 3. JSON roundtrip — import re-creates all claims
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_json_roundtrip(storage, tmp_path):
    await _seed_claims(storage)
    out = str(tmp_path / "export.json")

    from cortex.tools.export_import import export_json, import_json
    from cortex.storage.sqlite_adapter import SQLiteAdapter

    await export_json(storage, owner_id="eva", output_path=out)

    # Fresh empty storage for import
    db2 = tmp_path / "import.db"
    storage2 = SQLiteAdapter(db_path=str(db2))
    await storage2.initialize()

    result = await import_json(storage2, input_path=out, owner_id="eva")

    assert result.imported == 3
    assert result.skipped == 0
    assert result.errors == []
    assert not result.dry_run

    await storage2.close()


# ---------------------------------------------------------------------------
# 4. CSV export format — correct headers and row count
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_csv_export_headers(storage, tmp_path):
    await _seed_claims(storage)
    out = str(tmp_path / "claims.csv")

    from cortex.tools.export_import import export_csv
    count = await export_csv(storage, owner_id="eva", output_path=out)

    assert count == 3
    with open(out, newline="", encoding="utf-8") as fh:
        reader = csv.DictReader(fh)
        headers = reader.fieldnames or []

    expected = ["id", "subject", "predicate", "object_value", "category", "status", "confidence", "created_at"]
    for h in expected:
        assert h in headers, f"CSV missing header: {h}"


# ---------------------------------------------------------------------------
# 5. CSV export — row count matches claim count
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_csv_export_row_count(storage, tmp_path):
    await _seed_claims(storage)
    out = str(tmp_path / "claims.csv")

    from cortex.tools.export_import import export_csv
    await export_csv(storage, owner_id="eva", output_path=out)

    with open(out, newline="", encoding="utf-8") as fh:
        rows = list(csv.DictReader(fh))

    assert len(rows) == 3


# ---------------------------------------------------------------------------
# 6. Markdown export — grouped by category
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_markdown_export_categories(storage, tmp_path):
    await _seed_claims(storage)
    out = str(tmp_path / "memories.md")

    from cortex.tools.export_import import export_markdown
    count = await export_markdown(storage, owner_id="eva", output_path=out)

    assert count == 3
    content = Path(out).read_text()
    assert "# Cortex Memory Export" in content
    # Check at least two categories are present as headers
    assert "## " in content


# ---------------------------------------------------------------------------
# 7. Markdown export — claim content present
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_markdown_export_content(storage, tmp_path):
    await _seed_claims(storage)
    out = str(tmp_path / "memories.md")

    from cortex.tools.export_import import export_markdown
    await export_markdown(storage, owner_id="eva", output_path=out)

    content = Path(out).read_text()
    assert "Andrew" in content
    assert "dark mode" in content
    assert "Eva" in content


# ---------------------------------------------------------------------------
# 8. Mem0 import — basic field mapping
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_mem0_import_basic(storage, tmp_path):
    mem0_data = [
        {"id": "m1", "memory": "Andrew prefers dark mode", "metadata": {"category": "prefs"}, "created_at": "2026-01-01", "updated_at": "2026-01-01"},
        {"id": "m2", "memory": "Eva is an AI assistant",  "metadata": {"category": "identity"}, "created_at": "2026-01-01", "updated_at": "2026-01-01"},
    ]
    infile = tmp_path / "mem0.jsonl"
    infile.write_text("\n".join(json.dumps(r) for r in mem0_data), encoding="utf-8")

    from cortex.tools.export_import import import_mem0
    result = await import_mem0(storage, input_path=str(infile), owner_id="eva")

    assert result.imported == 2
    assert result.skipped == 0
    assert result.errors == []


# ---------------------------------------------------------------------------
# 9. Mem0 import — memory text becomes object_value
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_mem0_import_object_value(storage, tmp_path):
    mem0_data = [
        {"id": "m1", "memory": "Andrew loves coffee", "metadata": {}, "created_at": "2026-01-01", "updated_at": "2026-01-01"},
    ]
    infile = tmp_path / "mem0.jsonl"
    infile.write_text(json.dumps(mem0_data[0]), encoding="utf-8")

    from cortex.tools.export_import import import_mem0
    result = await import_mem0(storage, input_path=str(infile), owner_id="eva")

    assert result.imported == 1
    # Verify stored claim has correct object_value
    claims = await storage.get_claims_by_subject("Andrew", owner_id="eva")
    assert any(c.object_value == "Andrew loves coffee" for c in claims)


# ---------------------------------------------------------------------------
# 10. dry_run mode — no claims written
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_dry_run_no_writes(storage, tmp_path):
    await _seed_claims(storage)
    out = str(tmp_path / "export.json")

    from cortex.tools.export_import import export_json, import_json
    from cortex.storage.sqlite_adapter import SQLiteAdapter

    await export_json(storage, owner_id="eva", output_path=out)

    db2 = tmp_path / "dry.db"
    storage2 = SQLiteAdapter(db_path=str(db2))
    await storage2.initialize()

    result = await import_json(storage2, input_path=out, owner_id="eva", dry_run=True)

    assert result.dry_run is True
    assert result.imported == 3  # counted as would-be imports

    # Verify nothing was actually written
    conn = await storage2._get_conn()
    async with conn.execute("SELECT COUNT(*) FROM semantic_claim WHERE owner_id = 'eva'") as cur:
        row = await cur.fetchone()
    assert row[0] == 0

    await storage2.close()


# ---------------------------------------------------------------------------
# 11. Dedup on import — second import skips existing
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_dedup_on_import(storage, tmp_path):
    await _seed_claims(storage)
    out = str(tmp_path / "export.json")

    from cortex.tools.export_import import export_json, import_json
    await export_json(storage, owner_id="eva", output_path=out)

    # Import into the same storage that already has the claims
    result = await import_json(storage, input_path=out, owner_id="eva")

    assert result.imported == 0
    assert result.skipped == 3


# ---------------------------------------------------------------------------
# 12. Dedup on mem0 import — second import skips
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_dedup_mem0_import(storage, tmp_path):
    mem0_line = json.dumps({"id": "m1", "memory": "Andrew prefers dark mode", "metadata": {}, "created_at": "2026-01-01", "updated_at": "2026-01-01"})
    infile = tmp_path / "mem0.jsonl"
    infile.write_text(mem0_line, encoding="utf-8")

    from cortex.tools.export_import import import_mem0

    result1 = await import_mem0(storage, input_path=str(infile), owner_id="eva")
    assert result1.imported == 1

    result2 = await import_mem0(storage, input_path=str(infile), owner_id="eva")
    assert result2.imported == 0
    assert result2.skipped == 1


# ---------------------------------------------------------------------------
# 13. ImportResult __str__ representation
# ---------------------------------------------------------------------------

def test_import_result_str():
    from cortex.tools.export_import import ImportResult
    r = ImportResult(imported=5, skipped=2, errors=["oops"], dry_run=False)
    s = str(r)
    assert "imported=5" in s
    assert "skipped=2" in s
    assert "errors=1" in s

    r_dry = ImportResult(imported=3, skipped=0, dry_run=True)
    s_dry = str(r_dry)
    assert "DRY RUN" in s_dry


# ---------------------------------------------------------------------------
# 14. _infer_subject_predicate heuristic
# ---------------------------------------------------------------------------

def test_infer_subject_predicate():
    from cortex.tools.export_import import _infer_subject_predicate

    subject, predicate = _infer_subject_predicate("Andrew prefers dark mode")
    assert subject == "Andrew"
    assert predicate == "prefers"

    subject2, predicate2 = _infer_subject_predicate("Eva is an AI assistant")
    assert subject2 == "Eva"
    assert predicate2 == "is"

    # Fallback for unrecognized pattern
    subject3, predicate3 = _infer_subject_predicate("something completely random text here")
    assert isinstance(subject3, str) and len(subject3) > 0
    assert isinstance(predicate3, str) and len(predicate3) > 0


# ---------------------------------------------------------------------------
# 15. Mem0 import skips lines with empty memory field
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_mem0_import_empty_memory_skipped(storage, tmp_path):
    lines = [
        json.dumps({"id": "m1", "memory": "", "metadata": {}, "created_at": "2026-01-01", "updated_at": "2026-01-01"}),
        json.dumps({"id": "m2", "memory": "Eva is helpful", "metadata": {}, "created_at": "2026-01-01", "updated_at": "2026-01-01"}),
    ]
    infile = tmp_path / "mem0.jsonl"
    infile.write_text("\n".join(lines), encoding="utf-8")

    from cortex.tools.export_import import import_mem0
    result = await import_mem0(storage, input_path=str(infile), owner_id="eva")

    assert result.skipped == 1   # empty memory line skipped
    assert result.imported == 1  # the valid line
    assert len(result.errors) == 1  # error recorded for empty field


# ---------------------------------------------------------------------------
# 16. JSON import handles missing optional fields gracefully
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_json_import_minimal_claim(storage, tmp_path):
    """Import JSON with only required fields — should not crash."""
    payload = {
        "version": "1.0",
        "exported_at": "2026-01-01T00:00:00Z",
        "owner_id": "eva",
        "claims": [
            {
                "id": "abc123",
                "subject": "Andrew",
                "predicate": "uses",
                "object_value": "vim",
            }
        ],
        "entities": [],
        "cornerstones": [],
    }
    infile = tmp_path / "minimal.json"
    infile.write_text(json.dumps(payload), encoding="utf-8")

    from cortex.tools.export_import import import_json
    result = await import_json(storage, input_path=str(infile), owner_id="eva")

    assert result.imported == 1
    assert result.errors == []


# ---------------------------------------------------------------------------
# 17. export_json returns 0 for owner with no claims
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_json_export_empty_owner(storage, tmp_path):
    out = str(tmp_path / "empty.json")
    from cortex.tools.export_import import export_json
    count = await export_json(storage, owner_id="nobody", output_path=out)
    assert count == 0
    data = json.loads(Path(out).read_text())
    assert data["claims"] == []
