from __future__ import annotations

import base64
from datetime import datetime, timedelta, timezone
from pathlib import Path

import pytest

from osm_lead_source_service.errors import PbfDownloadRejected
from osm_lead_source_service.pbf import ApprovedTestPbfDownload, download_approved_test_pbf
from osm_lead_source_service.pbf.download import (
    _cache_metadata_path,
    _publish_cache_entry,
    _serialize_cache_metadata,
)

_FIXTURE = Path(__file__).parents[2] / "fixtures" / "pbf" / "tiny.osm.pbf.b64"
_SHA256 = "1f181fa63b838de88c5c7d8ecff2a014ca52a9349c6470fd2656ad3c9e0ddb8c"


def settings() -> ApprovedTestPbfDownload:
    return ApprovedTestPbfDownload(
        region_id="fixture",
        url="http://127.0.0.1:8765/tiny.osm.pbf",
        expected_sha256=_SHA256,
        expected_size_bytes=438,
    )


def _write_metadata(
    cache_path: Path,
    downloaded_at: datetime,
    *,
    etag: str | None = '"fixture-etag"',
    last_modified: str | None = "Sat, 11 Jul 2026 00:00:00 GMT",
) -> None:
    _cache_metadata_path(cache_path).write_bytes(
        _serialize_cache_metadata(settings(), downloaded_at, etag, last_modified)
    )


def test_valid_content_addressed_cache_preserves_metadata_and_avoids_network(
    tmp_path: Path,
) -> None:
    cache_path = tmp_path / f"{_SHA256}.osm.pbf"
    cache_path.write_bytes(base64.b64decode(_FIXTURE.read_text(encoding="ascii")))
    downloaded_at = datetime(2026, 7, 11, tzinfo=timezone.utc)
    _write_metadata(cache_path, downloaded_at)
    now = downloaded_at + timedelta(seconds=1)

    result = download_approved_test_pbf(settings(), tmp_path, clock=lambda: now)

    assert result.cache_hit is True
    assert result.local_path == cache_path
    assert result.downloaded_at == downloaded_at
    assert result.validated_at == now
    assert result.etag == '"fixture-etag"'
    assert result.last_modified == "Sat, 11 Jul 2026 00:00:00 GMT"


def test_incomplete_cache_is_rejected_without_network(tmp_path: Path) -> None:
    cache_path = tmp_path / f"{_SHA256}.osm.pbf"
    cache_path.write_bytes(base64.b64decode(_FIXTURE.read_text(encoding="ascii")))

    with pytest.raises(PbfDownloadRejected, match="cache entry is incomplete"):
        download_approved_test_pbf(settings(), tmp_path)


def test_invalid_cache_is_rejected_without_network(tmp_path: Path) -> None:
    cache_path = tmp_path / f"{_SHA256}.osm.pbf"
    cache_path.write_bytes(b"x" * 438)
    _write_metadata(cache_path, datetime(2026, 7, 11, tzinfo=timezone.utc))

    with pytest.raises(PbfDownloadRejected, match="checksum"):
        download_approved_test_pbf(settings(), tmp_path)


def test_cache_size_mismatch_is_rejected(tmp_path: Path) -> None:
    cache_path = tmp_path / f"{_SHA256}.osm.pbf"
    cache_path.write_bytes(b"x")
    _write_metadata(cache_path, datetime(2026, 7, 11, tzinfo=timezone.utc))

    with pytest.raises(PbfDownloadRejected, match="size"):
        download_approved_test_pbf(settings(), tmp_path)


def test_cache_directory_must_exist_and_not_be_symlink(tmp_path: Path) -> None:
    missing = tmp_path / "missing"
    with pytest.raises(PbfDownloadRejected, match="does not exist"):
        download_approved_test_pbf(settings(), missing)

    target = tmp_path / "real"
    target.mkdir()
    link = tmp_path / "link"
    link.symlink_to(target, target_is_directory=True)
    with pytest.raises(PbfDownloadRejected, match="symbolic link"):
        download_approved_test_pbf(settings(), link)


def test_cache_publication_never_replaces_existing_target(tmp_path: Path) -> None:
    temporary = tmp_path / ".download.part"
    temporary_metadata = tmp_path / ".metadata.part"
    target = tmp_path / f"{_SHA256}.osm.pbf"
    metadata_target = _cache_metadata_path(target)
    temporary.write_bytes(b"new")
    temporary_metadata.write_bytes(b"metadata")
    target.write_bytes(b"existing")

    with pytest.raises(PbfDownloadRejected, match="appeared during download"):
        _publish_cache_entry(
            temporary,
            target,
            temporary_metadata,
            metadata_target,
            datetime(2026, 7, 11, tzinfo=timezone.utc),
        )

    assert target.read_bytes() == b"existing"
    assert not metadata_target.exists()
    assert temporary.read_bytes() == b"new"
    assert temporary_metadata.read_bytes() == b"metadata"
