"""Tests for the backup status API endpoint logic."""

from local_deep_research.utilities.formatting import human_size


class TestHumanSize:
    """Tests for the shared human_size formatter."""

    def test_zero_bytes(self):
        assert human_size(0) == "0.0 B"

    def test_bytes(self):
        assert human_size(500) == "500.0 B"

    def test_kilobytes(self):
        assert human_size(1536) == "1.5 KB"

    def test_megabytes(self):
        result = human_size(258_179_072)
        assert "MB" in result
        assert result == "246.2 MB"

    def test_gigabytes(self):
        result = human_size(2_147_483_648)
        assert result == "2.0 GB"

    def test_terabytes(self):
        result = human_size(1_099_511_627_776)
        assert result == "1.0 TB"

    def test_petabytes(self):
        result = human_size(1_125_899_906_842_624)  # 1 PB = 1024^5
        assert result == "1.0 PB"

    def test_exabytes_fallback(self):
        result = human_size(1_152_921_504_606_846_976)  # 1 EB = 1024^6
        assert result == "1.0 EB"

    def test_negative_petabytes(self):
        result = human_size(-1_125_899_906_842_624)  # -1 PB
        assert result == "-1.0 PB"


class TestBackupStatusResponseShape:
    """Tests that verify backup status response structure using real filesystem."""

    def test_no_backups_returns_empty(self, tmp_path):
        """When backup directory is empty, response should have count=0."""
        backup_dir = tmp_path / "backups"
        backup_dir.mkdir()

        backups = sorted(
            backup_dir.glob("ldr_backup_*.db"),
            key=lambda p: p.name,
            reverse=True,
        )

        assert len(backups) == 0

    def test_single_backup_detected(self, tmp_path):
        """A single backup file should be found and sized correctly."""
        backup_dir = tmp_path / "backups"
        backup_dir.mkdir()

        backup = backup_dir / "ldr_backup_20260326_120000.db"
        backup.write_bytes(b"x" * 4096)

        backups = sorted(
            backup_dir.glob("ldr_backup_*.db"),
            key=lambda p: p.name,
            reverse=True,
        )

        assert len(backups) == 1
        assert backups[0].stat().st_size == 4096

    def test_multiple_backups_sorted_newest_first(self, tmp_path):
        """Multiple backups should sort newest first."""
        backup_dir = tmp_path / "backups"
        backup_dir.mkdir()

        (backup_dir / "ldr_backup_20260325_120000.db").write_bytes(b"old")
        (backup_dir / "ldr_backup_20260326_120000.db").write_bytes(b"new!")

        backups = sorted(
            backup_dir.glob("ldr_backup_*.db"),
            key=lambda p: p.name,
            reverse=True,
        )

        assert backups[0].name == "ldr_backup_20260326_120000.db"
        assert backups[1].name == "ldr_backup_20260325_120000.db"

    def test_tmp_files_not_included(self, tmp_path):
        """Temporary .tmp files should not appear in backup listing."""
        backup_dir = tmp_path / "backups"
        backup_dir.mkdir()

        (backup_dir / "ldr_backup_20260326_120000.db").write_bytes(b"real")
        (backup_dir / "ldr_backup_20260326_130000.db.tmp").write_bytes(b"temp")

        backups = list(backup_dir.glob("ldr_backup_*.db"))
        # .tmp should not match the *.db glob
        assert len(backups) == 1
        assert backups[0].name == "ldr_backup_20260326_120000.db"

    def test_total_size_calculation(self, tmp_path):
        """Total size should sum all backup file sizes."""
        backup_dir = tmp_path / "backups"
        backup_dir.mkdir()

        (backup_dir / "ldr_backup_20260325_120000.db").write_bytes(b"x" * 1000)
        (backup_dir / "ldr_backup_20260326_120000.db").write_bytes(b"x" * 2000)

        backups = list(backup_dir.glob("ldr_backup_*.db"))
        total = sum(b.stat().st_size for b in backups)

        assert total == 3000


class TestBackupStatusEndpointGlobHardening:
    """Real endpoint tests that drive GET /settings/api/backup-status.

    Unlike the response-shape tests above (which replicate the glob), these
    actually invoke the route so they verify the symlink hardening is wired
    into the handler, not just that pathlib behaves.
    """

    def test_symlinked_backup_entry_is_excluded(
        self, authenticated_client, tmp_path
    ):
        """A symlink planted in the backup dir must not appear in the API
        response, so an external target's metadata is never reported."""
        from local_deep_research.config.paths import (
            get_user_backup_directory,
        )

        with authenticated_client.session_transaction() as sess:
            username = sess.get("username")
        assert username, "authenticated_client should have a logged-in user"

        backup_dir = get_user_backup_directory(username)
        backup_dir.mkdir(parents=True, exist_ok=True)

        # A legitimate backup.
        real = backup_dir / "ldr_backup_20260101_120000.db"
        real.write_bytes(b"x" * 2048)

        # A malicious symlink whose name matches the glob but targets a file
        # outside the backup directory.
        outside = tmp_path / "outside_secret.db"
        outside.write_bytes(b"secret-data-of-known-size")
        evil = backup_dir / "ldr_backup_29991231_235959.db"
        evil.symlink_to(outside)

        resp = authenticated_client.get("/settings/api/backup-status")
        assert resp.status_code == 200
        data = resp.get_json()
        names = [b["filename"] for b in data["backups"]]

        assert "ldr_backup_20260101_120000.db" in names
        assert "ldr_backup_29991231_235959.db" not in names
        # The aggregate must never count more than what is listed — guards a
        # future regression where an entry is filtered from the list but its
        # (external) target size still leaks into total_size_bytes. Asserted as
        # an invariant (not an absolute byte count) so an async post-login
        # backup landing a real file in this dir can't make it flaky.
        assert data["total_size_bytes"] == sum(
            b["size_bytes"] for b in data["backups"]
        )
        # Listing must be non-destructive: the symlink and its target survive.
        assert evil.is_symlink()
        assert outside.exists()
