"""Tests for named manifest snapshots."""

from __future__ import annotations

import logging
from pathlib import Path

import pytest

from certisigma_census.snapshot import (
    _validate_name,
    create_snapshot,
    delete_snapshot,
    get_snapshot_path,
    list_snapshots,
)


@pytest.fixture
def sample_manifest(tmp_path: Path):
    """Create a minimal SQLite manifest for snapshot tests."""
    from certisigma_census.manifest import Manifest

    m = Manifest(root_dir=str(tmp_path / "data"))
    mpath = tmp_path / "test-manifest.db"
    m.save(mpath)
    m.close()
    return mpath


class TestValidateName:
    def test_valid_names(self):
        for name in ("q1-baseline", "Q1_2026", "snapshot-001", "abc"):
            _validate_name(name)

    def test_empty_name(self):
        with pytest.raises(ValueError, match="empty"):
            _validate_name("")

    def test_too_long(self):
        with pytest.raises(ValueError, match="too long"):
            _validate_name("a" * 200)

    def test_invalid_characters(self):
        with pytest.raises(ValueError, match="Invalid"):
            _validate_name("snap shot")

    def test_special_chars_rejected(self):
        for bad in ("../escape", "snap/shot", "snap.shot", "snap@1"):
            with pytest.raises(ValueError, match="Invalid"):
                _validate_name(bad)


class TestCreateSnapshot:
    def test_creates_db_and_meta(self, tmp_path: Path, sample_manifest: Path):
        sdir = tmp_path / "snapshots"
        info = create_snapshot("q1", sample_manifest, snapshot_dir=sdir)
        assert info.name == "q1"
        assert info.path.exists()
        assert (sdir / "q1.json").exists()
        assert info.size_bytes > 0

    def test_duplicate_name_raises(self, tmp_path: Path, sample_manifest: Path):
        sdir = tmp_path / "snapshots"
        create_snapshot("q1", sample_manifest, snapshot_dir=sdir)
        with pytest.raises(FileExistsError):
            create_snapshot("q1", sample_manifest, snapshot_dir=sdir)

    def test_missing_manifest_raises(self, tmp_path: Path):
        sdir = tmp_path / "snapshots"
        with pytest.raises(FileNotFoundError):
            create_snapshot("q1", tmp_path / "nonexistent.db", snapshot_dir=sdir)


class TestListSnapshots:
    def test_empty_dir(self, tmp_path: Path):
        assert list_snapshots(snapshot_dir=tmp_path / "empty") == []

    def test_lists_created_snapshots(self, tmp_path: Path, sample_manifest: Path):
        sdir = tmp_path / "snapshots"
        create_snapshot("alpha", sample_manifest, snapshot_dir=sdir)
        create_snapshot("beta", sample_manifest, snapshot_dir=sdir)
        snaps = list_snapshots(snapshot_dir=sdir)
        names = {s.name for s in snaps}
        assert names == {"alpha", "beta"}


class TestGetSnapshotPath:
    def test_returns_path_if_exists(self, tmp_path: Path, sample_manifest: Path):
        sdir = tmp_path / "snapshots"
        create_snapshot("q1", sample_manifest, snapshot_dir=sdir)
        p = get_snapshot_path("q1", snapshot_dir=sdir)
        assert p is not None
        assert p.exists()

    def test_returns_none_if_missing(self, tmp_path: Path):
        sdir = tmp_path / "snapshots"
        sdir.mkdir()
        assert get_snapshot_path("nonexistent", snapshot_dir=sdir) is None


class TestSnapshotIntegrity:
    def test_snapshot_is_valid_sqlite(self, tmp_path: Path, sample_manifest: Path):
        """Verify that snapshots are valid SQLite databases (sqlite3.backup)."""
        import sqlite3

        sdir = tmp_path / "snapshots"
        info = create_snapshot("integrity", sample_manifest, snapshot_dir=sdir)
        conn = sqlite3.connect(str(info.path))
        try:
            result = conn.execute("PRAGMA integrity_check").fetchone()
            assert result[0] == "ok"
        finally:
            conn.close()


class TestSnapshotCorruptMeta:
    def test_non_dict_meta_skipped(self, tmp_path: Path, sample_manifest: Path):
        """list_snapshots must skip metadata files with non-dict JSON (e.g. list)."""
        sdir = tmp_path / "snapshots"
        create_snapshot("good", sample_manifest, snapshot_dir=sdir)
        bad_meta = sdir / "bad.json"
        bad_meta.write_text("[1, 2, 3]", encoding="utf-8")
        (sdir / "bad.db").write_bytes(b"")
        snaps = list_snapshots(snapshot_dir=sdir)
        names = {s.name for s in snaps}
        assert "good" in names
        assert "bad" not in names

    def test_json_string_meta_skipped(self, tmp_path: Path, sample_manifest: Path):
        """list_snapshots must skip metadata files containing a JSON string."""
        sdir = tmp_path / "snapshots"
        create_snapshot("ok", sample_manifest, snapshot_dir=sdir)
        bad_meta = sdir / "bad.json"
        bad_meta.write_text('"just a string"', encoding="utf-8")
        (sdir / "bad.db").write_bytes(b"")
        snaps = list_snapshots(snapshot_dir=sdir)
        names = {s.name for s in snaps}
        assert "ok" in names
        assert "bad" not in names


class TestDeleteSnapshot:
    def test_deletes_existing(self, tmp_path: Path, sample_manifest: Path):
        sdir = tmp_path / "snapshots"
        create_snapshot("q1", sample_manifest, snapshot_dir=sdir)
        assert delete_snapshot("q1", snapshot_dir=sdir)
        assert not (sdir / "q1.db").exists()
        assert not (sdir / "q1.json").exists()

    def test_returns_false_if_missing(self, tmp_path: Path):
        sdir = tmp_path / "snapshots"
        sdir.mkdir()
        assert not delete_snapshot("nonexistent", snapshot_dir=sdir)

    def test_logs_when_unlink_fails(
        self, tmp_path: Path, sample_manifest: Path,
        monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
    ):
        """delete_snapshot must log a warning if file unlink fails."""
        sdir = tmp_path / "snapshots"
        create_snapshot("locked", sample_manifest, snapshot_dir=sdir)
        assert (sdir / "locked.db").exists()

        orig_unlink = Path.unlink

        def block_unlink(self_path: Path, *args, **kwargs) -> None:
            if "locked" in self_path.name:
                raise OSError("simulated permission denied")
            orig_unlink(self_path, *args, **kwargs)

        monkeypatch.setattr(Path, "unlink", block_unlink)

        with caplog.at_level(logging.WARNING, logger="certisigma_census.snapshot"):
            result = delete_snapshot("locked", snapshot_dir=sdir)

        assert not result
        assert "Cannot delete snapshot file" in caplog.text
