"""Tests for the evidence module — verify_hash(), integrity_check(), verify_manifest().

All SDK calls are mocked. No real API traffic.
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from certisigma_census.evidence import (
    IntegrityReport,
    ManifestVerifyEntry,
    ManifestVerifyReport,
    ModifiedFile,
    VerifyDetail,
    integrity_check,
    verify_hash,
    verify_manifest,
)
from certisigma_census.manifest import Manifest, ManifestEntry


SAMPLE_HASH = "ab" * 32


def _mock_verify_result(exists: bool = True):
    return MagicMock(
        exists=exists,
        hash_hex=SAMPLE_HASH,
        id="att_123" if exists else None,
        level="T2" if exists else None,
        timestamp="2026-01-15T10:00:00Z" if exists else None,
        source="inventory-hr" if exists else None,
    )


def _mock_evidence_result():
    return MagicMock(
        hash_hex=SAMPLE_HASH,
        level="T2",
        t0={"algorithm": "ECDSA-P256-SHA256", "signature": "3045022100abcdef"},
        t1={"tsaProvider": "QuoVadis", "timestamp": "2026-01-15T12:00:00Z"},
        t2={
            "bitcoinBlock": 876543,
            "bitcoinBlockHash": "0000000000000000000abcdef",
            "bitcoinTxid": "tx123abc",
            "otsProof": "dGVzdA==",
        },
        id="att_123",
    )


class TestVerifyHash:
    def test_verify_attested_hash(self):
        client = MagicMock()
        client.verify.return_value = _mock_verify_result(exists=True)
        client.get_evidence.return_value = _mock_evidence_result()

        detail = verify_hash(client, SAMPLE_HASH)

        assert detail.exists is True
        assert detail.hash_hex == SAMPLE_HASH
        assert detail.attestation_id == "att_123"
        assert detail.level == "T2"
        assert detail.t0 is not None
        assert detail.t1 is not None
        assert detail.t2 is not None
        assert detail.blockchain_url is not None
        assert detail.blockchain_tx_url is not None
        client.verify.assert_called_once_with(SAMPLE_HASH)
        client.get_evidence.assert_called_once_with("att_123")

    def test_verify_unattested_hash(self):
        client = MagicMock()
        client.verify.return_value = _mock_verify_result(exists=False)

        detail = verify_hash(client, SAMPLE_HASH)

        assert detail.exists is False
        assert detail.attestation_id is None
        assert detail.t0 is None
        assert detail.t1 is None
        assert detail.t2 is None
        client.get_evidence.assert_not_called()

    def test_verify_without_evidence_fetch(self):
        client = MagicMock()
        client.verify.return_value = _mock_verify_result(exists=True)

        detail = verify_hash(client, SAMPLE_HASH, fetch_evidence=False)

        assert detail.exists is True
        assert detail.attestation_id == "att_123"
        assert detail.t0 is None
        client.get_evidence.assert_not_called()

    def test_verify_evidence_fetch_failure_graceful(self):
        """If get_evidence fails, verify still returns basic data."""
        client = MagicMock()
        client.verify.return_value = _mock_verify_result(exists=True)
        client.get_evidence.side_effect = Exception("API timeout")

        detail = verify_hash(client, SAMPLE_HASH)

        assert detail.exists is True
        assert detail.attestation_id == "att_123"
        assert detail.t0 is None

    def test_verify_detail_dataclass_defaults(self):
        d = VerifyDetail(hash_hex="aa" * 32, exists=False)
        assert d.attestation_id is None
        assert d.level is None
        assert d.blockchain_url is None


class TestIntegrityCheck:
    def _make_manifest_and_files(self, tmp_path: Path) -> Manifest:
        """Create a manifest with 3 files, all present on disk."""
        root = tmp_path / "data"
        root.mkdir()
        files = {
            "doc.pdf": b"original content of doc.pdf",
            "sheet.csv": b"id,name\n1,alpha\n2,beta",
            "notes.txt": b"meeting notes from 2026",
        }
        m = Manifest(root_dir=str(root))
        for name, content in files.items():
            fpath = root / name
            fpath.write_bytes(content)
            from certisigma import hash_file
            h = hash_file(str(fpath))
            stat = fpath.stat()
            m.add(ManifestEntry(
                hash_hex=h, path=name,
                size=stat.st_size, mtime=stat.st_mtime,
            ))
        return m

    def test_integrity_all_unchanged(self, tmp_path):
        m = self._make_manifest_and_files(tmp_path)
        try:
            report = integrity_check(m)

            assert report.unchanged == 3
            assert len(report.modified) == 0
            assert len(report.missing) == 0
            assert len(report.new_files) == 0
            assert not report.has_discrepancies
        finally:
            m.close()

    def test_integrity_modified_file(self, tmp_path):
        m = self._make_manifest_and_files(tmp_path)
        try:
            root = Path(m.root_dir)
            (root / "doc.pdf").write_bytes(b"TAMPERED CONTENT!")

            report = integrity_check(m)

            assert report.unchanged == 2
            assert len(report.modified) == 1
            assert report.modified[0].path == "doc.pdf"
            assert report.modified[0].expected_hash != report.modified[0].actual_hash
            assert report.has_discrepancies
        finally:
            m.close()

    def test_integrity_missing_file(self, tmp_path):
        m = self._make_manifest_and_files(tmp_path)
        try:
            root = Path(m.root_dir)
            (root / "notes.txt").unlink()

            report = integrity_check(m)

            assert "notes.txt" in report.missing
            assert report.has_discrepancies
        finally:
            m.close()

    def test_integrity_new_file(self, tmp_path):
        m = self._make_manifest_and_files(tmp_path)
        try:
            root = Path(m.root_dir)
            (root / "intruder.exe").write_bytes(b"malicious payload")

            report = integrity_check(m)

            assert "intruder.exe" in report.new_files
            assert report.has_discrepancies
        finally:
            m.close()

    def test_integrity_combined_discrepancies(self, tmp_path):
        m = self._make_manifest_and_files(tmp_path)
        try:
            root = Path(m.root_dir)
            (root / "doc.pdf").write_bytes(b"changed")
            (root / "notes.txt").unlink()
            (root / "new.dat").write_bytes(b"new file")

            report = integrity_check(m)

            assert len(report.modified) == 1
            assert len(report.missing) == 1
            assert len(report.new_files) == 1
            assert report.has_discrepancies
        finally:
            m.close()

    def test_integrity_empty_manifest(self, tmp_path):
        root = tmp_path / "empty"
        root.mkdir()
        m = Manifest(root_dir=str(root))
        try:
            report = integrity_check(m)

            assert report.total_in_manifest == 0
            assert report.unchanged == 0
            assert not report.has_discrepancies
        finally:
            m.close()

    def test_integrity_unreadable_file(self, tmp_path):
        m = self._make_manifest_and_files(tmp_path)
        try:
            root = Path(m.root_dir)
            locked = root / "doc.pdf"
            locked.chmod(0o000)

            try:
                report = integrity_check(m)
                assert len(report.errors) >= 1
            finally:
                locked.chmod(0o644)
        finally:
            m.close()


class TestVerifyManifest:
    """Tests for verify_manifest() — full-chain manifest verification."""

    @staticmethod
    def _make_manifest(entries: list[tuple[str, str]]) -> Manifest:
        m = Manifest(root_dir="/test")
        for hash_hex, path in entries:
            m.add(ManifestEntry(hash_hex=hash_hex, path=path, size=100, mtime=1.0))
        return m

    def test_all_verified(self):
        entries = [("aa" * 32, "a.txt"), ("bb" * 32, "b.txt")]
        m = self._make_manifest(entries)
        try:
            client = MagicMock()
            batch_result = MagicMock()
            batch_result.results = [
                {"hash_hex": "aa" * 32, "exists": True, "id": "att_1", "level": "T2", "timestamp": "2026-01-01", "source": "scan"},
                {"hash_hex": "bb" * 32, "exists": True, "id": "att_2", "level": "T1", "timestamp": "2026-01-02", "source": "scan"},
            ]
            client.batch_verify.return_value = batch_result

            report = verify_manifest(m, client)

            assert report.total == 2
            assert report.verified == 2
            assert report.not_found == 0
            assert report.errors == 0
            assert not report.has_discrepancies
        finally:
            m.close()

    def test_some_not_found(self):
        entries = [("aa" * 32, "a.txt"), ("cc" * 32, "c.txt")]
        m = self._make_manifest(entries)
        try:
            client = MagicMock()
            batch_result = MagicMock()
            batch_result.results = [
                {"hash_hex": "aa" * 32, "exists": True, "id": "att_1", "level": "T0"},
                {"hash_hex": "cc" * 32, "exists": False},
            ]
            client.batch_verify.return_value = batch_result

            report = verify_manifest(m, client)

            assert report.total == 2
            assert report.verified == 1
            assert report.not_found == 1
            assert report.has_discrepancies
        finally:
            m.close()

    def test_api_error_graceful(self):
        entries = [("aa" * 32, "a.txt")]
        m = self._make_manifest(entries)
        try:
            client = MagicMock()
            client.batch_verify.side_effect = Exception("API down")

            report = verify_manifest(m, client)

            assert report.total == 1
            assert report.errors == 1
            assert report.verified == 0
            assert report.has_discrepancies
        finally:
            m.close()

    def test_empty_manifest(self):
        m = Manifest(root_dir="/empty")
        try:
            client = MagicMock()

            report = verify_manifest(m, client)

            assert report.total == 0
            assert report.verified == 0
            assert not report.has_discrepancies
            client.batch_verify.assert_not_called()
        finally:
            m.close()

    def test_dedup_hashes_in_manifest(self):
        """Manifest deduplicates by hash_hex (PRIMARY KEY), so adding
        the same hash twice keeps only the last path."""
        same_hash = "dd" * 32
        m = Manifest(root_dir="/test")
        try:
            m.add(ManifestEntry(hash_hex=same_hash, path="copy1.txt", size=100, mtime=1.0))
            m.add(ManifestEntry(hash_hex=same_hash, path="copy2.txt", size=100, mtime=1.0))

            client = MagicMock()
            batch_result = MagicMock()
            batch_result.results = [
                {"hash_hex": same_hash, "exists": True, "id": "att_1", "level": "T2"},
            ]
            client.batch_verify.return_value = batch_result

            report = verify_manifest(m, client)

            assert report.total == 1
            assert report.verified == 1
            client.batch_verify.assert_called_once()
        finally:
            m.close()

    def test_batch_chunking(self):
        """Verify that >100 hashes are split into multiple batches."""
        entries = [(f"{i:064x}", f"file_{i}.txt") for i in range(150)]
        m = self._make_manifest(entries)
        try:
            client = MagicMock()

            def mock_batch_verify(hashes, **kwargs):
                result = MagicMock()
                result.results = [
                    {"hash_hex": h, "exists": True, "id": f"att_{h[:8]}", "level": "T1"}
                    for h in hashes
                ]
                return result

            client.batch_verify.side_effect = mock_batch_verify

            report = verify_manifest(m, client)

            assert report.total == 150
            assert report.verified == 150
            assert client.batch_verify.call_count == 2
        finally:
            m.close()

    def test_report_dataclass_defaults(self):
        r = ManifestVerifyReport()
        assert r.total == 0
        assert r.verified == 0
        assert not r.has_discrepancies

    def test_entry_dataclass(self):
        e = ManifestVerifyEntry(hash_hex="aa" * 32, path="test.txt", verified=True, level="T2")
        assert e.verified is True
        assert e.level == "T2"
        assert e.error is None
