"""Tests for manifest creation, serialization, and round-trip (SQLite backend)."""

import json
from pathlib import Path

import pytest

from certisigma_census.manifest import Manifest, ManifestEntry


def test_add_and_get():
    m = Manifest(root_dir="/tmp/test")
    try:
        entry = ManifestEntry(hash_hex="ab" * 32, path="file.txt", size=1024, mtime=1.0)
        m.add(entry)
        assert m.total == 1
        got = m.get("ab" * 32)
        assert got is not None
        assert got.hash_hex == entry.hash_hex
        assert got.path == entry.path
        assert got.size == entry.size
        assert m.get("00" * 32) is None
    finally:
        m.close()


def test_mark_attested():
    m = Manifest(root_dir="/tmp/test")
    try:
        entry = ManifestEntry(hash_hex="ab" * 32, path="file.txt", size=1024, mtime=1.0)
        m.add(entry)
        assert m.attested_count == 0
        m.mark_attested("ab" * 32, "att_42", "2026-01-01T00:00:00Z")
        assert m.attested_count == 1
        refreshed = m.get("ab" * 32)
        assert refreshed.attested is True
        assert refreshed.attestation_id == "att_42"
    finally:
        m.close()


def test_save_and_load(tmp_path: Path):
    m = Manifest(root_dir="/data/sensitive")
    try:
        for i in range(5):
            h = f"{i:064x}"
            m.add(ManifestEntry(hash_hex=h, path=f"file_{i}.dat", size=i * 100, mtime=float(i)))
        m.mark_attested(f"{0:064x}", "att_1", "2026-01-01T00:00:00Z")

        fpath = tmp_path / "manifest.db"
        m.save(fpath)

        with Manifest.load(fpath) as loaded:
            assert loaded.total == 5
            assert loaded.attested_count == 1
            assert loaded.root_dir == "/data/sensitive"
            assert loaded.get(f"{0:064x}").attested is True
    finally:
        m.close()


def test_duplicate_hash_overwrites():
    m = Manifest(root_dir="/tmp")
    try:
        e1 = ManifestEntry(hash_hex="ab" * 32, path="a.txt", size=100, mtime=1.0)
        e2 = ManifestEntry(hash_hex="ab" * 32, path="b.txt", size=200, mtime=2.0)
        m.add(e1)
        m.add(e2)
        assert m.total == 1
        assert m.get("ab" * 32).path == "b.txt"
    finally:
        m.close()


# ─── has_path ──────────────────────────────────────────────────────────────

def test_has_path_found():
    m = Manifest(root_dir="/tmp")
    try:
        m.add(ManifestEntry(hash_hex="aa" * 32, path="docs/report.pdf", size=500, mtime=1.0))
        m.add(ManifestEntry(hash_hex="bb" * 32, path="data.csv", size=200, mtime=2.0))

        found = m.has_path("docs/report.pdf")
        assert found is not None
        assert found.hash_hex == "aa" * 32
    finally:
        m.close()


def test_has_path_not_found():
    m = Manifest(root_dir="/tmp")
    try:
        m.add(ManifestEntry(hash_hex="aa" * 32, path="file.txt", size=100, mtime=1.0))
        assert m.has_path("other.txt") is None
    finally:
        m.close()


# ─── remove_by_path ──────────────────────────────────────────────────────

def test_remove_by_path():
    m = Manifest(root_dir="/tmp")
    try:
        m.add(ManifestEntry(hash_hex="aa" * 32, path="file.txt", size=100, mtime=1.0))
        assert m.total == 1
        assert m.remove_by_path("file.txt") is True
        assert m.total == 0
        assert m.remove_by_path("nonexistent.txt") is False
    finally:
        m.close()


# ─── unattested_hashes ────────────────────────────────────────────────────

def test_unattested_hashes():
    m = Manifest(root_dir="/tmp")
    try:
        m.add(ManifestEntry(hash_hex="aa" * 32, path="a.txt", size=100, mtime=1.0))
        m.add(ManifestEntry(hash_hex="bb" * 32, path="b.txt", size=200, mtime=2.0))
        m.mark_attested("aa" * 32, "att_1", "2026-01-01")
        hashes = m.unattested_hashes()
        assert hashes == ["bb" * 32]
    finally:
        m.close()


# ─── iter_entries ─────────────────────────────────────────────────────────

def test_iter_entries():
    m = Manifest(root_dir="/tmp")
    try:
        for i in range(3):
            m.add(ManifestEntry(hash_hex=f"{i:064x}", path=f"f{i}.txt", size=i * 10, mtime=float(i)))
        entries = list(m.iter_entries())
        assert len(entries) == 3
        assert all(isinstance(e, ManifestEntry) for e in entries)
    finally:
        m.close()


# ─── merge ─────────────────────────────────────────────────────────────────

def test_merge_adds_new_entries():
    base = Manifest(root_dir="/tmp")
    incoming = Manifest(root_dir="/tmp")
    try:
        base.add(ManifestEntry(hash_hex="aa" * 32, path="a.txt", size=100, mtime=1.0))
        base.mark_attested("aa" * 32, "att_1", "2026-01-01T00:00:00Z")

        incoming.add(ManifestEntry(hash_hex="bb" * 32, path="b.txt", size=200, mtime=2.0))

        changed = base.merge(incoming)
        assert changed == 1
        assert base.total == 2
        assert base.get("aa" * 32).attested is True
        assert base.get("bb" * 32).attested is False
    finally:
        incoming.close()
        base.close()


def test_merge_preserves_attestation_on_unchanged():
    base = Manifest(root_dir="/tmp")
    incoming = Manifest(root_dir="/tmp")
    try:
        base.add(ManifestEntry(hash_hex="aa" * 32, path="a.txt", size=100, mtime=1.0))
        base.mark_attested("aa" * 32, "att_1", "2026-01-01T00:00:00Z")

        incoming.add(ManifestEntry(hash_hex="aa" * 32, path="a.txt", size=100, mtime=1.0))

        changed = base.merge(incoming)
        assert changed == 0
        assert base.get("aa" * 32).attested is True
        assert base.get("aa" * 32).attestation_id == "att_1"
    finally:
        incoming.close()
        base.close()


def test_merge_updates_changed_file():
    """Same hash but different path/size is treated as an update."""
    base = Manifest(root_dir="/tmp")
    incoming = Manifest(root_dir="/tmp")
    try:
        base.add(ManifestEntry(hash_hex="aa" * 32, path="a.txt", size=100, mtime=1.0))
        base.mark_attested("aa" * 32, "att_1", "2026-01-01T00:00:00Z")

        incoming.add(ManifestEntry(hash_hex="aa" * 32, path="a_renamed.txt", size=100, mtime=3.0))

        changed = base.merge(incoming)
        assert changed == 1
        entry = base.get("aa" * 32)
        assert entry.path == "a_renamed.txt"
        assert entry.attested is True
    finally:
        incoming.close()
        base.close()


# ─── SQLite persistence ──────────────────────────────────────────────────

def test_save_creates_db(tmp_path: Path):
    """save() creates a .db SQLite file."""
    m = Manifest(root_dir="/tmp")
    try:
        m.add(ManifestEntry(hash_hex="aa" * 32, path="f.txt", size=100, mtime=1.0))

        fpath = tmp_path / "manifest.db"
        m.save(fpath)
        assert fpath.exists()
        header = fpath.read_bytes()[:6]
        assert header == b"SQLite"
    finally:
        m.close()


def test_save_with_json_extension_creates_db(tmp_path: Path):
    """save('foo.json') creates foo.db (SQLite), not a JSON file."""
    m = Manifest(root_dir="/tmp")
    try:
        m.add(ManifestEntry(hash_hex="aa" * 32, path="f.txt", size=100, mtime=1.0))

        fpath = tmp_path / "manifest.json"
        m.save(fpath)
        db_path = tmp_path / "manifest.db"
        assert db_path.exists()
    finally:
        m.close()


# ─── JSON migration ──────────────────────────────────────────────────────

def test_load_migrates_json_to_sqlite(tmp_path: Path):
    """Loading a legacy JSON manifest auto-migrates to SQLite."""
    json_data = {
        "version": "1",
        "created_at": 1700000000.0,
        "updated_at": 1700000001.0,
        "root_dir": "/data/test",
        "entries": {
            "aa" * 32: {
                "hash_hex": "aa" * 32,
                "path": "doc.pdf",
                "size": 512,
                "mtime": 1.0,
                "attested": True,
                "attestation_id": "att_1",
                "attested_at": "2026-01-01",
            },
            "bb" * 32: {
                "hash_hex": "bb" * 32,
                "path": "data.csv",
                "size": 256,
                "mtime": 2.0,
                "attested": False,
                "attestation_id": None,
                "attested_at": None,
            },
        },
    }
    json_path = tmp_path / "manifest.json"
    json_path.write_text(json.dumps(json_data), encoding="utf-8")

    with Manifest.load(json_path) as loaded:
        assert loaded.total == 2
        assert loaded.root_dir == "/data/test"
        assert loaded.attested_count == 1
        assert loaded.get("aa" * 32).attested is True
        assert loaded.get("aa" * 32).attestation_id == "att_1"

    assert (tmp_path / "manifest.db").exists()
    assert (tmp_path / "manifest.json.bak").exists()
    assert not json_path.exists()


def test_schema_version(tmp_path: Path):
    """SQLite manifest uses PRAGMA user_version = 2."""
    import sqlite3

    m = Manifest(root_dir="/tmp")
    try:
        m.add(ManifestEntry(hash_hex="aa" * 32, path="f.txt", size=100, mtime=1.0))
        db_path = tmp_path / "manifest.db"
        m.save(db_path)

        conn = sqlite3.connect(str(db_path))
        try:
            uv = conn.execute("PRAGMA user_version").fetchone()[0]
        finally:
            conn.close()
        assert uv == 2
    finally:
        m.close()


# ─── close() ──────────────────────────────────────────────────────────────

def test_close_prevents_further_operations():
    """close() closes the DB connection; subsequent operations raise."""
    import sqlite3

    m = Manifest(root_dir="/tmp")
    m.add(ManifestEntry(hash_hex="aa" * 32, path="f.txt", size=100, mtime=1.0))
    m.close()
    with pytest.raises(sqlite3.ProgrammingError):
        m.total


# ─── load() error paths ──────────────────────────────────────────────────

def test_load_nonexistent_raises():
    """Manifest.load() with a nonexistent path raises FileNotFoundError."""
    with pytest.raises(FileNotFoundError):
        Manifest.load(Path("/nonexistent/path/manifest.db"))
