"""
Comprehensive tests for FileIntegrityManager to increase coverage.

Tests all public and internal methods with mocked DB sessions and verifiers.
"""

import pytest
from unittest.mock import patch, MagicMock
from pathlib import Path
from datetime import datetime, UTC
from contextlib import contextmanager


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_mock_session():
    """Create a mock DB session with chainable query interface."""
    session = MagicMock()
    return session


@contextmanager
def _mock_session_cm(session):
    """Context manager wrapper for mock session."""
    yield session


def _make_verifier(
    should=True,
    file_type="faiss_index",
    algorithm="sha256",
    checksum="abc123",
    allows_mod=False,
):
    """Create a mock verifier with configurable behavior."""
    v = MagicMock()
    v.should_verify.return_value = should
    v.get_file_type.return_value = file_type
    v.get_algorithm.return_value = algorithm
    v.calculate_checksum.return_value = checksum
    v.allows_modifications.return_value = allows_mod
    return v


def _make_record(**overrides):
    """Create a mock FileIntegrityRecord with sensible defaults."""
    defaults = dict(
        id=1,
        file_path="/resolved/path",
        file_type="faiss_index",
        checksum="abc123",
        algorithm="sha256",
        file_size=1024,
        file_mtime=1000.0,
        verify_on_load=True,
        allow_modifications=False,
        total_verifications=5,
        last_verified_at=datetime(2025, 1, 1, tzinfo=UTC),
        last_verification_passed=True,
        consecutive_successes=3,
        consecutive_failures=0,
        created_at=datetime(2025, 1, 1, tzinfo=UTC),
        updated_at=None,
        related_entity_type=None,
        related_entity_id=None,
    )
    defaults.update(overrides)
    record = MagicMock()
    for k, v in defaults.items():
        setattr(record, k, v)
    return record


# Patch path for the session context used inside integrity_manager module
_SESSION_PATCH = (
    "local_deep_research.security.file_integrity.integrity_manager"
    ".get_user_db_session"
)
_HAS_CTX_PATCH = (
    "local_deep_research.security.file_integrity.integrity_manager"
    "._has_session_context"
)


@pytest.fixture
def integrity_manager():
    """
    Build a FileIntegrityManager with mocked session context.

    Yields (manager, session). Patcher is automatically stopped on teardown.
    """
    session = _make_mock_session()

    with patch(_HAS_CTX_PATCH, True), patch(_SESSION_PATCH) as mock_get_session:
        mock_get_session.return_value = _mock_session_cm(session)
        # cleanup_all_old_failures queries count then returns
        session.query.return_value.count.return_value = 0
        from local_deep_research.security.file_integrity.integrity_manager import (
            FileIntegrityManager,
        )

        mgr = FileIntegrityManager(username="testuser", password="testpass")

    # Re-patch for subsequent calls
    patcher = patch(
        _SESSION_PATCH, side_effect=lambda u, p: _mock_session_cm(session)
    )
    patcher.start()
    mgr._test_session = session
    yield mgr, session
    patcher.stop()


def _build_manager(session=None):
    """
    Build a FileIntegrityManager with mocked session context.

    Returns (manager, session). Caller MUST call mgr._test_patcher.stop()
    or use the integrity_manager fixture instead.

    NOTE: Prefer the integrity_manager fixture for new tests.
    This helper is kept for TestInit tests that need custom session setup.
    """
    if session is None:
        session = _make_mock_session()

    with patch(_HAS_CTX_PATCH, True), patch(_SESSION_PATCH) as mock_get_session:
        mock_get_session.return_value = _mock_session_cm(session)
        # cleanup_all_old_failures queries count then returns
        session.query.return_value.count.return_value = 0
        from local_deep_research.security.file_integrity.integrity_manager import (
            FileIntegrityManager,
        )

        mgr = FileIntegrityManager(username="testuser", password="testpass")

    # Re-patch for subsequent calls
    patcher = patch(
        _SESSION_PATCH, side_effect=lambda u, p: _mock_session_cm(session)
    )
    patcher.start()
    mgr._test_patcher = patcher
    mgr._test_session = session
    return mgr, session


# ---------------------------------------------------------------------------
# Tests: __init__
# ---------------------------------------------------------------------------


class TestInit:
    def test_raises_import_error_when_no_session_context(self):
        with patch(_HAS_CTX_PATCH, False):
            from local_deep_research.security.file_integrity.integrity_manager import (
                FileIntegrityManager,
            )

            with pytest.raises(ImportError, match="requires Flask"):
                FileIntegrityManager(username="user")

    def test_successful_init_sets_attributes(self):
        mgr, _session = _build_manager()
        assert mgr.username == "testuser"
        assert mgr.password == "testpass"
        assert mgr.verifiers == []
        mgr._test_patcher.stop()

    def test_startup_cleanup_logs_when_deleted(self):
        session = _make_mock_session()
        # First call to cleanup_all_old_failures: count returns > MAX
        with (
            patch(_HAS_CTX_PATCH, True),
            patch(_SESSION_PATCH) as mock_get_session,
        ):
            mock_get_session.return_value = _mock_session_cm(session)
            # total_failures > MAX_TOTAL_FAILURES to trigger deletion
            session.query.return_value.count.return_value = 10005
            session.query.return_value.order_by.return_value.limit.return_value.all.return_value = [
                MagicMock() for _ in range(5)
            ]
            from local_deep_research.security.file_integrity.integrity_manager import (
                FileIntegrityManager,
            )

            mgr = FileIntegrityManager(username="user")
        mgr._test_patcher = patch(_SESSION_PATCH)  # dummy
        # No assertion needed beyond not raising

    def test_startup_cleanup_handles_exception(self):
        """If startup cleanup raises, __init__ should still succeed."""
        with (
            patch(_HAS_CTX_PATCH, True),
            patch(_SESSION_PATCH) as mock_get_session,
        ):
            mock_get_session.side_effect = RuntimeError("db down")
            from local_deep_research.security.file_integrity.integrity_manager import (
                FileIntegrityManager,
            )

            # Should not raise thanks to try/except in __init__
            mgr = FileIntegrityManager(username="user")
            assert mgr.username == "user"


# ---------------------------------------------------------------------------
# Tests: _normalize_path
# ---------------------------------------------------------------------------


class TestNormalizePath:
    def test_returns_resolved_string(self, integrity_manager):
        mgr, _ = integrity_manager
        p = Path("/some/../some/file.txt")
        result = mgr._normalize_path(p)
        assert result == str(p.resolve())
        assert isinstance(result, str)

    def test_relative_path_becomes_absolute(self, integrity_manager):
        mgr, _ = integrity_manager
        p = Path("relative/file.txt")
        result = mgr._normalize_path(p)
        assert Path(result).is_absolute()


# ---------------------------------------------------------------------------
# Tests: register_verifier
# ---------------------------------------------------------------------------


class TestRegisterVerifier:
    def test_appends_verifier(self, integrity_manager):
        mgr, _ = integrity_manager
        v1 = _make_verifier()
        v2 = _make_verifier(file_type="pdf")
        mgr.register_verifier(v1)
        mgr.register_verifier(v2)
        assert mgr.verifiers == [v1, v2]


# ---------------------------------------------------------------------------
# Tests: _get_verifier_for_file
# ---------------------------------------------------------------------------


class TestGetVerifierForFile:
    def test_returns_matching_verifier(self, integrity_manager):
        mgr, _ = integrity_manager
        v1 = _make_verifier(should=False)
        v2 = _make_verifier(should=True, file_type="pdf")
        mgr.register_verifier(v1)
        mgr.register_verifier(v2)
        result = mgr._get_verifier_for_file(Path("/some/file.pdf"))
        assert result is v2

    def test_returns_first_matching(self, integrity_manager):
        mgr, _ = integrity_manager
        v1 = _make_verifier(should=True, file_type="faiss")
        v2 = _make_verifier(should=True, file_type="pdf")
        mgr.register_verifier(v1)
        mgr.register_verifier(v2)
        result = mgr._get_verifier_for_file(Path("/file"))
        assert result is v1

    def test_returns_none_when_no_match(self, integrity_manager):
        mgr, _ = integrity_manager
        v1 = _make_verifier(should=False)
        mgr.register_verifier(v1)
        result = mgr._get_verifier_for_file(Path("/file"))
        assert result is None

    def test_returns_none_with_no_verifiers(self, integrity_manager):
        mgr, _ = integrity_manager
        result = mgr._get_verifier_for_file(Path("/file"))
        assert result is None


# ---------------------------------------------------------------------------
# Tests: _needs_verification
# ---------------------------------------------------------------------------


class TestNeedsVerification:
    def test_file_missing_returns_true(self, integrity_manager):
        mgr, _ = integrity_manager
        record = _make_record()
        path = MagicMock(spec=Path)
        path.exists.return_value = False
        assert mgr._needs_verification(record, path) is True

    def test_never_verified_returns_true(self, integrity_manager):
        mgr, _ = integrity_manager
        record = _make_record(last_verified_at=None)
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        assert mgr._needs_verification(record, path) is True

    def test_no_stored_mtime_returns_true(self, integrity_manager):
        mgr, _ = integrity_manager
        record = _make_record(file_mtime=None)
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.stat.return_value.st_mtime = 1000.0
        assert mgr._needs_verification(record, path) is True

    def test_mtime_changed_returns_true(self, integrity_manager):
        mgr, _ = integrity_manager
        record = _make_record(file_mtime=1000.0)
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.stat.return_value.st_mtime = 2000.0  # clearly different
        assert mgr._needs_verification(record, path) is True

    def test_mtime_unchanged_returns_false(self, integrity_manager):
        mgr, _ = integrity_manager
        record = _make_record(file_mtime=1000.0)
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.stat.return_value.st_mtime = 1000.0
        assert mgr._needs_verification(record, path) is False

    def test_mtime_tiny_float_difference_returns_false(self, integrity_manager):
        """Differences within 0.001 threshold should not trigger verification."""
        mgr, _ = integrity_manager
        record = _make_record(file_mtime=1000.0)
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.stat.return_value.st_mtime = 1000.0005  # within threshold
        assert mgr._needs_verification(record, path) is False


# ---------------------------------------------------------------------------
# Tests: _do_verification
# ---------------------------------------------------------------------------


class TestDoVerification:
    def test_file_missing(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record()
        path = MagicMock(spec=Path)
        path.exists.return_value = False
        passed, reason = mgr._do_verification(record, path, session)
        assert passed is False
        assert reason == "file_missing"

    def test_no_verifier(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record()
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        # No verifiers registered
        passed, reason = mgr._do_verification(record, path, session)
        assert passed is False
        assert reason == "no_verifier"

    def test_checksum_match(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(checksum="abc123")
        mgr.register_verifier(v)
        record = _make_record(checksum="abc123")
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.stat.return_value.st_mtime = 1234.0
        passed, reason = mgr._do_verification(record, path, session)
        assert passed is True
        assert reason is None
        # mtime should be updated on record
        assert record.file_mtime == 1234.0

    def test_checksum_mismatch(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(checksum="different_checksum")
        mgr.register_verifier(v)
        record = _make_record(checksum="abc123")
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        passed, reason = mgr._do_verification(record, path, session)
        assert passed is False
        assert reason == "checksum_mismatch"

    def test_checksum_calculation_exception(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier()
        v.calculate_checksum.side_effect = IOError("read error")
        mgr.register_verifier(v)
        record = _make_record()
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        passed, reason = mgr._do_verification(record, path, session)
        assert passed is False
        assert "checksum_calculation_failed" in reason
        assert "read error" in reason


# ---------------------------------------------------------------------------
# Tests: _update_stats
# ---------------------------------------------------------------------------


class TestUpdateStats:
    def test_passed(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record(
            total_verifications=5,
            consecutive_successes=2,
            consecutive_failures=1,
        )
        mgr._update_stats(record, True, session)
        assert record.total_verifications == 6
        assert record.consecutive_successes == 3
        assert record.consecutive_failures == 0
        assert record.last_verification_passed is True
        assert record.last_verified_at is not None

    def test_failed(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record(
            total_verifications=10,
            consecutive_successes=5,
            consecutive_failures=0,
        )
        mgr._update_stats(record, False, session)
        assert record.total_verifications == 11
        assert record.consecutive_successes == 0
        assert record.consecutive_failures == 1
        assert record.last_verification_passed is False


# ---------------------------------------------------------------------------
# Tests: _log_failure
# ---------------------------------------------------------------------------


class TestLogFailure:
    def test_logs_with_existing_file(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(checksum="actual_cs")
        mgr.register_verifier(v)
        record = _make_record(id=1, checksum="expected_cs")
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.stat.return_value.st_size = 2048
        # Setup session for cleanup queries
        session.query.return_value.filter_by.return_value.count.return_value = 0

        mgr._log_failure(record, path, "checksum_mismatch", session)

        session.add.assert_called_once()
        failure_obj = session.add.call_args[0][0]
        assert failure_obj.expected_checksum == "expected_cs"
        assert failure_obj.actual_checksum == "actual_cs"
        assert failure_obj.file_size == 2048
        assert failure_obj.failure_reason == "checksum_mismatch"

    def test_logs_with_missing_file(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record(id=1, checksum="expected_cs")
        path = MagicMock(spec=Path)
        path.exists.return_value = False
        session.query.return_value.filter_by.return_value.count.return_value = 0

        mgr._log_failure(record, path, "file_missing", session)

        session.add.assert_called_once()
        failure_obj = session.add.call_args[0][0]
        assert failure_obj.actual_checksum is None
        assert failure_obj.file_size is None
        assert failure_obj.failure_reason == "file_missing"

    def test_logs_with_verifier_exception(self, integrity_manager):
        """If verifier.calculate_checksum raises, actual_checksum stays None."""
        mgr, session = integrity_manager
        v = _make_verifier()
        v.calculate_checksum.side_effect = IOError("cannot read")
        mgr.register_verifier(v)
        record = _make_record(id=1)
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        session.query.return_value.filter_by.return_value.count.return_value = 0

        mgr._log_failure(record, path, "error", session)

        failure_obj = session.add.call_args[0][0]
        assert failure_obj.actual_checksum is None

    def test_triggers_global_cleanup_on_id_mod_100(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record(id=200)  # 200 % 100 == 0
        path = MagicMock(spec=Path)
        path.exists.return_value = False
        # Per-file cleanup: count under limit
        session.query.return_value.filter_by.return_value.count.return_value = 0
        # Global cleanup: count under threshold
        session.query.return_value.count.return_value = 5000

        mgr._log_failure(record, path, "test", session)

        # count() was called for both per-file and global checks
        assert (
            session.query.return_value.count.called
            or session.query.return_value.filter_by.return_value.count.called
        )


# ---------------------------------------------------------------------------
# Tests: _cleanup_old_failures
# ---------------------------------------------------------------------------


class TestCleanupOldFailures:
    def test_no_cleanup_when_under_limit(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record(id=1)
        session.query.return_value.filter_by.return_value.count.return_value = (
            50
        )
        mgr._cleanup_old_failures(record, session)
        # Should not attempt to delete
        session.delete.assert_not_called()

    def test_cleanup_when_over_limit(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record(id=1)
        session.query.return_value.filter_by.return_value.count.return_value = (
            110
        )
        old_failures = [MagicMock() for _ in range(10)]
        session.query.return_value.filter_by.return_value.order_by.return_value.limit.return_value.all.return_value = old_failures

        mgr._cleanup_old_failures(record, session)

        assert session.delete.call_count == 10

    def test_cleanup_at_exactly_limit(self, integrity_manager):
        """Exactly at MAX_FAILURES_PER_FILE should not trigger cleanup."""
        mgr, session = integrity_manager
        record = _make_record(id=1)
        session.query.return_value.filter_by.return_value.count.return_value = (
            100
        )
        mgr._cleanup_old_failures(record, session)
        session.delete.assert_not_called()


# ---------------------------------------------------------------------------
# Tests: _check_global_cleanup_needed
# ---------------------------------------------------------------------------


class TestCheckGlobalCleanupNeeded:
    def test_no_cleanup_under_threshold(self, integrity_manager):
        mgr, session = integrity_manager
        # threshold = 10000 * 1.2 = 12000
        session.query.return_value.count.return_value = 11000
        mgr._check_global_cleanup_needed(session)
        session.delete.assert_not_called()

    def test_cleanup_over_threshold(self, integrity_manager):
        mgr, session = integrity_manager
        session.query.return_value.count.return_value = 13000  # > 12000
        old_failures = [MagicMock() for _ in range(3000)]  # 13000 - 10000
        session.query.return_value.order_by.return_value.limit.return_value.all.return_value = old_failures
        mgr._check_global_cleanup_needed(session)
        assert session.delete.call_count == 3000

    def test_exactly_at_threshold_no_cleanup(self, integrity_manager):
        mgr, session = integrity_manager
        session.query.return_value.count.return_value = 12000  # == threshold
        mgr._check_global_cleanup_needed(session)
        session.delete.assert_not_called()


# ---------------------------------------------------------------------------
# Tests: record_file
# ---------------------------------------------------------------------------


class TestRecordFile:
    def test_new_file(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(
            checksum="newcs", file_type="faiss_index", algorithm="sha256"
        )
        mgr.register_verifier(v)
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.resolve.return_value = Path("/resolved/new_file")
        path.stat.return_value.st_size = 4096
        path.stat.return_value.st_mtime = 2000.0
        # No existing record
        session.query.return_value.filter_by.return_value.first.return_value = (
            None
        )

        mgr.record_file(
            path, related_entity_type="rag_index", related_entity_id=42
        )

        session.add.assert_called_once()
        session.commit.assert_called_once()
        session.refresh.assert_called_once()

    def test_existing_file_updates(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(checksum="updatedcs")
        mgr.register_verifier(v)
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.resolve.return_value = Path("/resolved/existing")
        path.stat.return_value.st_size = 8192
        path.stat.return_value.st_mtime = 3000.0
        existing_record = _make_record()
        session.query.return_value.filter_by.return_value.first.return_value = (
            existing_record
        )

        mgr.record_file(path)

        assert existing_record.checksum == "updatedcs"
        assert existing_record.file_size == 8192
        assert existing_record.file_mtime == 3000.0
        session.commit.assert_called_once()
        # session.add should NOT be called for updates
        session.add.assert_not_called()

    def test_file_not_found(self, integrity_manager):
        mgr, _ = integrity_manager
        path = MagicMock(spec=Path)
        path.exists.return_value = False
        with pytest.raises(FileNotFoundError, match="File not found"):
            mgr.record_file(path)

    def test_no_verifier(self, integrity_manager):
        mgr, _ = integrity_manager
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        # No verifiers registered
        with pytest.raises(ValueError, match="No verifier registered"):
            mgr.record_file(path)


# ---------------------------------------------------------------------------
# Tests: verify_file
# ---------------------------------------------------------------------------


class TestVerifyFile:
    def test_no_record_creates_one(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(checksum="cs")
        mgr.register_verifier(v)
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.resolve.return_value = Path("/resolved/path")
        path.stat.return_value.st_size = 100
        path.stat.return_value.st_mtime = 500.0
        # First query returns no record, triggering record_file
        session.query.return_value.filter_by.return_value.first.return_value = (
            None
        )

        passed, reason = mgr.verify_file(path)

        assert passed is True
        assert reason is None

    def test_no_record_create_fails(self, integrity_manager):
        mgr, session = integrity_manager
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.resolve.return_value = Path("/resolved/path")
        # No record found
        session.query.return_value.filter_by.return_value.first.return_value = (
            None
        )
        # No verifiers -> record_file will raise ValueError

        passed, reason = mgr.verify_file(path)

        assert passed is False
        assert "Failed to create integrity record" in reason

    def test_skips_if_unchanged(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record(file_mtime=1000.0)
        session.query.return_value.filter_by.return_value.first.return_value = (
            record
        )
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.resolve.return_value = Path("/resolved/path")
        path.stat.return_value.st_mtime = 1000.0  # unchanged

        passed, reason = mgr.verify_file(path)

        assert passed is True
        assert reason is None

    def test_verify_passes(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(checksum="abc123")
        mgr.register_verifier(v)
        record = _make_record(
            checksum="abc123",
            file_mtime=1000.0,
            total_verifications=5,
            consecutive_successes=2,
            consecutive_failures=0,
        )
        session.query.return_value.filter_by.return_value.first.return_value = (
            record
        )
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.resolve.return_value = Path("/resolved/path")
        path.stat.return_value.st_mtime = (
            2000.0  # changed, triggers verification
        )

        passed, reason = mgr.verify_file(path, force=True)

        assert passed is True
        assert reason is None
        session.commit.assert_called()

    def test_verify_fails(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(checksum="wrong")
        mgr.register_verifier(v)
        record = _make_record(
            id=1,
            checksum="abc123",
            file_mtime=1000.0,
            total_verifications=5,
            consecutive_successes=2,
            consecutive_failures=0,
        )
        session.query.return_value.filter_by.return_value.first.return_value = (
            record
        )
        # Per-file cleanup count
        session.query.return_value.filter_by.return_value.count.return_value = 0
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.resolve.return_value = Path("/resolved/path")
        path.stat.return_value.st_mtime = 2000.0
        path.stat.return_value.st_size = 1024

        passed, reason = mgr.verify_file(path, force=True)

        assert passed is False
        assert reason == "checksum_mismatch"
        session.commit.assert_called()

    def test_force_overrides_skip(self, integrity_manager):
        """force=True should verify even when mtime unchanged."""
        mgr, session = integrity_manager
        v = _make_verifier(checksum="abc123")
        mgr.register_verifier(v)
        record = _make_record(checksum="abc123", file_mtime=1000.0)
        session.query.return_value.filter_by.return_value.first.return_value = (
            record
        )
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.resolve.return_value = Path("/resolved/path")
        path.stat.return_value.st_mtime = 1000.0  # unchanged

        passed, reason = mgr.verify_file(path, force=True)

        assert passed is True
        # Should have called _do_verification (verifier was used)
        v.calculate_checksum.assert_called_once()


# ---------------------------------------------------------------------------
# Tests: update_checksum
# ---------------------------------------------------------------------------


class TestUpdateChecksum:
    def test_success(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(checksum="newcs")
        mgr.register_verifier(v)
        record = _make_record()
        session.query.return_value.filter_by.return_value.first.return_value = (
            record
        )
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.stat.return_value.st_size = 5000
        path.stat.return_value.st_mtime = 9000.0

        mgr.update_checksum(path)

        assert record.checksum == "newcs"
        assert record.file_size == 5000
        assert record.file_mtime == 9000.0
        session.commit.assert_called_once()

    def test_file_not_found(self, integrity_manager):
        mgr, _ = integrity_manager
        path = MagicMock(spec=Path)
        path.exists.return_value = False
        with pytest.raises(FileNotFoundError):
            mgr.update_checksum(path)

    def test_no_verifier(self, integrity_manager):
        mgr, _ = integrity_manager
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        with pytest.raises(ValueError, match="No verifier registered"):
            mgr.update_checksum(path)

    def test_no_record(self, integrity_manager):
        mgr, session = integrity_manager
        v = _make_verifier(checksum="cs")
        mgr.register_verifier(v)
        session.query.return_value.filter_by.return_value.first.return_value = (
            None
        )
        path = MagicMock(spec=Path)
        path.exists.return_value = True
        path.stat.return_value.st_size = 100
        path.stat.return_value.st_mtime = 100.0
        with pytest.raises(ValueError, match="No integrity record exists"):
            mgr.update_checksum(path)


# ---------------------------------------------------------------------------
# Tests: get_file_stats
# ---------------------------------------------------------------------------


class TestGetFileStats:
    def test_with_record(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record(
            total_verifications=10,
            last_verified_at=datetime(2025, 6, 1, tzinfo=UTC),
            last_verification_passed=True,
            consecutive_successes=5,
            consecutive_failures=0,
            file_type="faiss_index",
            created_at=datetime(2025, 1, 1, tzinfo=UTC),
        )
        session.query.return_value.filter_by.return_value.first.return_value = (
            record
        )

        result = mgr.get_file_stats(Path("/some/file"))

        assert result is not None
        assert result["total_verifications"] == 10
        assert result["consecutive_successes"] == 5
        assert result["consecutive_failures"] == 0
        assert result["file_type"] == "faiss_index"
        assert result["last_verification_passed"] is True

    def test_without_record(self, integrity_manager):
        mgr, session = integrity_manager
        session.query.return_value.filter_by.return_value.first.return_value = (
            None
        )

        result = mgr.get_file_stats(Path("/nonexistent"))

        assert result is None


# ---------------------------------------------------------------------------
# Tests: get_failure_history
# ---------------------------------------------------------------------------


class TestGetFailureHistory:
    def test_with_records(self, integrity_manager):
        mgr, session = integrity_manager
        record = _make_record(id=1)
        session.query.return_value.filter_by.return_value.first.return_value = (
            record
        )
        failures = [MagicMock(), MagicMock()]
        session.query.return_value.filter_by.return_value.order_by.return_value.limit.return_value.all.return_value = failures

        result = mgr.get_failure_history(Path("/some/file"), limit=50)

        assert len(result) == 2
        # Each failure should be expunged from session
        assert session.expunge.call_count == 2

    def test_without_record(self, integrity_manager):
        mgr, session = integrity_manager
        session.query.return_value.filter_by.return_value.first.return_value = (
            None
        )

        result = mgr.get_failure_history(Path("/no/record"))

        assert result == []

    def test_default_limit(self, integrity_manager):
        """Default limit should be 100."""
        mgr, session = integrity_manager
        record = _make_record(id=1)
        session.query.return_value.filter_by.return_value.first.return_value = (
            record
        )
        session.query.return_value.filter_by.return_value.order_by.return_value.limit.return_value.all.return_value = []

        mgr.get_failure_history(Path("/file"))

        # Verify limit was called via the chain
        session.query.return_value.filter_by.return_value.order_by.return_value.limit.assert_called()


# ---------------------------------------------------------------------------
# Tests: cleanup_all_old_failures
# ---------------------------------------------------------------------------


class TestCleanupAllOldFailures:
    def test_under_limit(self, integrity_manager):
        mgr, session = integrity_manager
        session.query.return_value.count.return_value = 5000  # under 10000

        result = mgr.cleanup_all_old_failures()

        assert result == 0
        session.delete.assert_not_called()

    def test_over_limit(self, integrity_manager):
        mgr, session = integrity_manager
        session.query.return_value.count.return_value = 10500
        old_failures = [MagicMock() for _ in range(500)]
        session.query.return_value.order_by.return_value.limit.return_value.all.return_value = old_failures

        result = mgr.cleanup_all_old_failures()

        assert result == 500
        assert session.delete.call_count == 500
        session.commit.assert_called()

    def test_exactly_at_limit(self, integrity_manager):
        mgr, session = integrity_manager
        session.query.return_value.count.return_value = 10000

        result = mgr.cleanup_all_old_failures()

        assert result == 0


# ---------------------------------------------------------------------------
# Tests: get_total_failure_count
# ---------------------------------------------------------------------------


class TestGetTotalFailureCount:
    def test_returns_count(self, integrity_manager):
        mgr, session = integrity_manager
        session.query.return_value.count.return_value = 42

        result = mgr.get_total_failure_count()

        assert result == 42

    def test_returns_zero(self, integrity_manager):
        mgr, session = integrity_manager
        session.query.return_value.count.return_value = 0

        result = mgr.get_total_failure_count()

        assert result == 0
