"""Tests for the forensic archive module."""

from __future__ import annotations

import json
import zipfile
from pathlib import Path

import pytest
from click.testing import CliRunner

from certisigma_census.archive import (
    ArchiveResult,
    ChainOfCustody,
    create_archive,
    verify_archive,
)
from certisigma_census.cli import main
from certisigma_census.manifest import Manifest, ManifestEntry


def _make_manifest(tmp_path: Path, *, count: int = 3) -> Path:
    db = tmp_path / "manifest.db"
    m = Manifest(root_dir=str(tmp_path))
    for i in range(count):
        h = f"{i:064x}"
        m.add(ManifestEntry(hash_hex=h, path=f"file_{i}.txt", size=100 + i, mtime=1.0 + i))
    m.save(db)
    m.close()
    return db


class TestCreateArchive:
    def test_basic_archive(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        result = create_archive(db, out)

        assert out.exists()
        assert result.manifest_entries == 3
        assert result.archive_size > 0
        assert len(result.archive_hash) == 64

    def test_archive_contents(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        create_archive(db, out)

        with zipfile.ZipFile(out) as zf:
            names = zf.namelist()
            assert "manifest.db" in names
            assert "metadata.json" in names
            assert "chain_of_custody.json" in names
            assert "inventory.json" in names
            assert "SHA256SUMS" in names

    def test_chain_of_custody(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        coc = ChainOfCustody(examiner="J. Doe", case_id="CASE-42", notes="Test")
        result = create_archive(db, out, chain_of_custody=coc)

        assert result.chain_of_custody.examiner == "J. Doe"
        with zipfile.ZipFile(out) as zf:
            coc_data = json.loads(zf.read("chain_of_custody.json"))
            assert coc_data["examiner"] == "J. Doe"
            assert coc_data["case_id"] == "CASE-42"

    def test_metadata_has_version(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        create_archive(db, out)

        with zipfile.ZipFile(out) as zf:
            meta = json.loads(zf.read("metadata.json"))
            assert "census_version" in meta
            assert meta["manifest_entries"] == 3

    def test_inventory_entries(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path, count=5)
        out = tmp_path / "evidence.zip"
        create_archive(db, out)

        with zipfile.ZipFile(out) as zf:
            inv = json.loads(zf.read("inventory.json"))
            assert len(inv) == 5
            assert all("hash_hex" in e for e in inv)
            assert all("path" in e for e in inv)

    def test_no_compression(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        create_archive(db, out, compress=False)
        assert out.exists()

    def test_missing_manifest_raises(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            create_archive(tmp_path / "missing.db", tmp_path / "out.zip")

    def test_include_seal(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        seal_path = db.parent / (db.name + ".seal")
        seal_path.write_text('{"hmac": "test"}')

        out = tmp_path / "evidence.zip"
        create_archive(db, out, include_seal=True)

        with zipfile.ZipFile(out) as zf:
            assert "manifest.db.seal" in zf.namelist()

    def test_exclude_seal(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        seal_path = db.parent / (db.name + ".seal")
        seal_path.write_text('{"hmac": "test"}')

        out = tmp_path / "evidence.zip"
        create_archive(db, out, include_seal=False)

        with zipfile.ZipFile(out) as zf:
            assert "manifest.db.seal" not in zf.namelist()


class TestVerifyArchive:
    def test_valid_archive(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        create_archive(db, out)

        result = verify_archive(out)
        assert result["valid"] is True
        assert all(f["valid"] for f in result["files"].values())

    def test_tampered_archive(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        create_archive(db, out)

        tampered = tmp_path / "tampered.zip"
        with zipfile.ZipFile(out, "r") as zr, zipfile.ZipFile(tampered, "w") as zw:
            for name in zr.namelist():
                data = zr.read(name)
                if name == "manifest.db":
                    data = b"corrupted"
                zw.writestr(name, data)

        result = verify_archive(tampered)
        assert result["valid"] is False

    def test_missing_archive(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            verify_archive(tmp_path / "missing.zip")


class TestArchiveCLI:
    def test_archive_command(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "test-archive.zip"

        runner = CliRunner()
        result = runner.invoke(main, [
            "archive", str(db), "-o", str(out),
            "--examiner", "Test User", "--case-id", "CASE-1",
        ])
        assert result.exit_code == 0
        assert "Archive created" in result.output
        assert out.exists()

    def test_verify_archive_command(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "test-archive.zip"
        create_archive(db, out)

        runner = CliRunner()
        result = runner.invoke(main, ["verify-archive", str(out)])
        assert result.exit_code == 0
        assert "VALID" in result.output

    def test_archive_help(self) -> None:
        runner = CliRunner()
        result = runner.invoke(main, ["archive", "--help"])
        assert result.exit_code == 0
        assert "--examiner" in result.output
        assert "--case-id" in result.output

    def test_verify_archive_help(self) -> None:
        runner = CliRunner()
        result = runner.invoke(main, ["verify-archive", "--help"])
        assert result.exit_code == 0
        assert "SHA256SUMS" in result.output

    def test_archive_json_output(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"

        runner = CliRunner()
        result = runner.invoke(main, [
            "archive", str(db), "-o", str(out), "--json",
        ])
        assert result.exit_code == 0
        data = json.loads(result.output)
        assert "path" in data
        assert data["manifest_entries"] == 3
        assert "archive_hash" in data
        assert "census_version" in data
        assert "elapsed_seconds" in data

    def test_verify_archive_json_output(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        create_archive(db, out)

        runner = CliRunner()
        result = runner.invoke(main, ["verify-archive", str(out), "--json"])
        assert result.exit_code == 0
        data = json.loads(result.output)
        assert data["valid"] is True
        assert "census_version" in data
        assert "elapsed_seconds" in data

    def test_verify_archive_json_tampered(self, tmp_path: Path) -> None:
        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        create_archive(db, out)

        tampered = tmp_path / "tampered.zip"
        with zipfile.ZipFile(out, "r") as zr, zipfile.ZipFile(tampered, "w") as zw:
            for name in zr.namelist():
                data = zr.read(name)
                if name == "manifest.db":
                    data = b"corrupted"
                zw.writestr(name, data)

        runner = CliRunner()
        result = runner.invoke(main, ["verify-archive", str(tampered), "--json"])
        assert result.exit_code == 1
        data = json.loads(result.output)
        assert data["valid"] is False


class TestArchivePermissions:
    def test_archive_file_permissions(self, tmp_path: Path) -> None:
        import os
        import stat

        db = _make_manifest(tmp_path)
        out = tmp_path / "evidence.zip"
        create_archive(db, out)

        mode = os.stat(out).st_mode
        assert stat.S_IMODE(mode) == 0o600


class TestArchiveSizeGuard:
    def test_oversized_archive_rejected(self, tmp_path: Path) -> None:
        from certisigma_census.archive import MAX_ARCHIVE_SIZE

        fake_zip = tmp_path / "huge.zip"
        fake_zip.write_bytes(b"x")

        from unittest.mock import patch as _patch
        with _patch("certisigma_census.archive.MAX_ARCHIVE_SIZE", 0):
            with pytest.raises(ValueError, match="too large"):
                from certisigma_census.archive import verify_archive
                verify_archive(fake_zip)
