from __future__ import annotations

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

import pytest

from osm_lead_source_service.domain.enums import OsmObjectType
from osm_lead_source_service.domain.models import OsmIdentity
from osm_lead_source_service.errors import ContractViolation, PbfDownloadRejected
from osm_lead_source_service.pbf import (
    ApprovedTestPbfDownload,
    ParsedExtractMetadata,
    ParsedOsmObject,
    PbfObjectCounts,
    PbfParseLimits,
    PbfParseSummary,
    RelationMember,
    ValidatedPbfDownload,
    VerifiedPbfInput,
)


def test_parse_limits_reject_unbounded_values() -> None:
    with pytest.raises(ContractViolation, match="hard limit"):
        PbfParseLimits(max_file_size_bytes=(4 * 1024 * 1024 * 1024) + 1)
    with pytest.raises(ContractViolation, match="max_tags_per_object"):
        PbfParseLimits(max_total_tags=2, max_tags_per_object=3)
    assert PbfParseLimits(max_tags_per_object=512).max_tags_per_object == 512
    with pytest.raises(ContractViolation, match="max_tags_per_object.*hard limit"):
        PbfParseLimits(max_tags_per_object=1_025)
    with pytest.raises(ContractViolation, match="envelope hard limit"):
        PbfParseLimits(max_uncompressed_blob_bytes=33 * 1024 * 1024)


def test_parsed_object_shapes_are_type_specific() -> None:
    node = ParsedOsmObject(
        identity=OsmIdentity(OsmObjectType.NODE, 1),
        version=1,
        tags=(("name", "B"), ("amenity", "cafe")),
        longitude_e7=100,
        latitude_e7=200,
    )
    assert node.tags == (("amenity", "cafe"), ("name", "B"))

    with pytest.raises(ContractViolation, match="node coordinates"):
        ParsedOsmObject(identity=OsmIdentity(OsmObjectType.NODE, 1), version=1)
    with pytest.raises(ContractViolation, match="ways cannot"):
        ParsedOsmObject(
            identity=OsmIdentity(OsmObjectType.WAY, 2),
            version=1,
            longitude_e7=1,
            latitude_e7=1,
        )
    with pytest.raises(ContractViolation, match="relations cannot"):
        ParsedOsmObject(
            identity=OsmIdentity(OsmObjectType.RELATION, 3),
            version=1,
            node_refs=(1,),
        )

    with pytest.raises(ContractViolation, match="longitude_e7"):
        ParsedOsmObject(
            identity=OsmIdentity(OsmObjectType.NODE, 4),
            version=1,
            longitude_e7=True,
            latitude_e7=0,
        )


def test_relation_member_validates_identity() -> None:
    member = RelationMember(OsmObjectType.WAY, 10, "outer")
    assert member.ref == 10
    with pytest.raises(ContractViolation):
        RelationMember(OsmObjectType.WAY, 0)


def test_download_settings_are_loopback_only_and_secret_safe() -> None:
    settings = ApprovedTestPbfDownload(
        region_id="fixture",
        url="http://127.0.0.1:8765/tiny.osm.pbf",
        expected_sha256="a" * 64,
        expected_size_bytes=10,
    )
    assert "127.0.0.1" not in repr(settings)
    assert settings.url.endswith(".osm.pbf")

    rejected = [
        "https://127.0.0.1:8765/tiny.osm.pbf",
        "http://download.geofabrik.de:80/test.osm.pbf",
        "http://localhost:8765/tiny.osm.pbf",
        "http://127.0.0.1:bad/tiny.osm.pbf",
        "http://user:pass@127.0.0.1:8765/tiny.osm.pbf",
        "http://127.0.0.1:8765/tiny.osm.pbf?token=secret",
        "http://127.0.0.1/tiny.osm.pbf",
        "http://127.0.0.1:8765/tiny.zip",
    ]
    for url in rejected:
        with pytest.raises(PbfDownloadRejected):
            ApprovedTestPbfDownload(
                region_id="fixture",
                url=url,
                expected_sha256="a" * 64,
                expected_size_bytes=10,
            )


def test_verified_input_is_explicit_and_download_converts_to_it() -> None:
    start = datetime(2026, 7, 11, tzinfo=timezone.utc)
    download = ValidatedPbfDownload(
        region_id="fixture",
        source_url="http://127.0.0.1:8765/tiny.osm.pbf",
        local_path=Path(f"/tmp/{'a' * 64}.osm.pbf"),
        content_sha256="a" * 64,
        size_bytes=10,
        downloaded_at=start,
        validated_at=start,
    )
    approved = download.as_verified_input()
    assert isinstance(approved, VerifiedPbfInput)
    assert approved.content_sha256 == "a" * 64
    assert approved.size_bytes == 10
    assert approved.local_path == download.local_path
    assert "fixture" in approved.provenance


def test_parsed_extract_metadata_requires_matching_download_and_parse() -> None:
    start = datetime(2026, 7, 11, tzinfo=timezone.utc)
    download = ValidatedPbfDownload(
        region_id="fixture",
        source_url="http://127.0.0.1:8765/tiny.osm.pbf",
        local_path=Path(f"/tmp/{'a' * 64}.osm.pbf"),
        content_sha256="a" * 64,
        size_bytes=10,
        downloaded_at=start,
        validated_at=start + timedelta(seconds=1),
    )
    summary = PbfParseSummary(
        content_sha256="a" * 64,
        size_bytes=10,
        counts=PbfObjectCounts(nodes=1),
        total_tags=2,
        parser_started_at=start + timedelta(seconds=2),
        parser_completed_at=start + timedelta(seconds=3),
    )
    metadata = ParsedExtractMetadata.from_results(download, summary)
    assert metadata.counts.total == 1

    bad_summary = PbfParseSummary(
        content_sha256="b" * 64,
        size_bytes=10,
        counts=PbfObjectCounts(),
        total_tags=0,
        parser_started_at=start + timedelta(seconds=2),
        parser_completed_at=start + timedelta(seconds=3),
    )
    with pytest.raises(ContractViolation, match="SHA-256"):
        ParsedExtractMetadata.from_results(download, bad_summary)


def test_extract_metadata_is_not_a_reconciliation_snapshot() -> None:
    assert not hasattr(ParsedExtractMetadata, "reconciliation_completed_at")
    assert not hasattr(ParsedExtractMetadata, "completeness_status")
