"""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")
    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


def test_mark_attested():
    m = Manifest(root_dir="/tmp/test")
    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"


def test_save_and_load(tmp_path: Path):
    m = Manifest(root_dir="/data/sensitive")
    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)

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


def test_duplicate_hash_overwrites():
    m = Manifest(root_dir="/tmp")
    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"


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

def test_has_path_found():
    m = Manifest(root_dir="/tmp")
    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


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


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

def test_remove_by_path():
    m = Manifest(root_dir="/tmp")
    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


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

def test_unattested_hashes():
    m = Manifest(root_dir="/tmp")
    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]


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

def test_iter_entries():
    m = Manifest(root_dir="/tmp")
    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)


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

def test_merge_adds_new_entries():
    base = Manifest(root_dir="/tmp")
    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 = Manifest(root_dir="/tmp")
    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


def test_merge_preserves_attestation_on_unchanged():
    base = Manifest(root_dir="/tmp")
    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 = Manifest(root_dir="/tmp")
    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"


def test_merge_updates_changed_file():
    """Same hash but different path/size is treated as an update."""
    base = Manifest(root_dir="/tmp")
    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 = Manifest(root_dir="/tmp")
    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


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

def test_save_creates_db(tmp_path: Path):
    """save() creates a .db SQLite file."""
    m = Manifest(root_dir="/tmp")
    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"


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")
    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()


# ─── 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")

    loaded = Manifest.load(json_path)
    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")
    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))
    uv = conn.execute("PRAGMA user_version").fetchone()[0]
    conn.close()
    assert uv == 2


# ─── 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"))
