from __future__ import annotations

import dataclasses
import pathlib
from unittest.mock import MagicMock

import pytest
import requests
from PIL import Image

from planemapper.display import NullDisplay
from planemapper.main import _run_one_cycle
from planemapper.models import Aircraft
from planemapper.renderer.projection import MapBounds
from planemapper.renderer.renderer import Renderer

FIXTURE_DIR = pathlib.Path(__file__).parent / "fixtures"


@pytest.fixture
def renderer(monkeypatch: pytest.MonkeyPatch) -> Renderer:
    monkeypatch.setattr(
        "planemapper.renderer.airspace.AIRSPACE_PATH",
        FIXTURE_DIR / "airspace_sample.geojson",
    )
    base_map = Image.new("RGB", (800, 480), color=(255, 255, 255))
    bounds = MapBounds(home_lat=53.0, home_lon=-6.0, radius_nm=100.0)
    return Renderer(base_map, bounds)


def _make_aircraft(icao: str = "ABC123") -> Aircraft:
    return Aircraft(icao=icao, lat=53.1, lon=-6.1, altitude_ft=10000, callsign="TST1")


def test_timeout_returns_stale_last_aircraft(renderer: Renderer) -> None:
    display = NullDisplay()
    last = [_make_aircraft()]
    mock_fetcher = MagicMock()
    mock_fetcher.fetch.side_effect = requests.Timeout
    result = _run_one_cycle(renderer, mock_fetcher, display, last)
    assert len(result) == 1
    assert result[0].is_stale is True


def test_empty_fetch_returns_stale_when_had_previous(renderer: Renderer) -> None:
    display = NullDisplay()
    last = [_make_aircraft()]
    mock_fetcher = MagicMock()
    mock_fetcher.fetch.return_value = []
    result = _run_one_cycle(renderer, mock_fetcher, display, last)
    assert len(result) == 1
    assert result[0].is_stale is True


def test_stale_aircraft_rendered_without_exception(renderer: Renderer) -> None:
    display = NullDisplay()
    stale_aircraft = dataclasses.replace(_make_aircraft(), is_stale=True)
    mock_fetcher = MagicMock()
    mock_fetcher.fetch.side_effect = requests.Timeout
    result = _run_one_cycle(renderer, mock_fetcher, display, [stale_aircraft])
    assert result[0].is_stale is True


def test_stale_cycle_completes_without_crash(renderer: Renderer) -> None:
    display = NullDisplay()
    last = [_make_aircraft()]
    mock_fetcher = MagicMock()
    mock_fetcher.fetch.side_effect = requests.Timeout
    # Should complete without any exception
    result = _run_one_cycle(renderer, mock_fetcher, display, last)
    assert isinstance(result, list)


def test_recovery_after_timeout_returns_fresh(renderer: Renderer) -> None:
    display = NullDisplay()
    last = [_make_aircraft()]
    mock_fetcher = MagicMock()
    # Cycle 1: timeout → stale
    mock_fetcher.fetch.side_effect = requests.Timeout
    stale_result = _run_one_cycle(renderer, mock_fetcher, display, last)
    assert stale_result[0].is_stale is True
    # Cycle 2: fresh fetch → recovery
    fresh_aircraft = [_make_aircraft(icao="DEF456")]
    mock_fetcher.fetch.side_effect = None
    mock_fetcher.fetch.return_value = fresh_aircraft
    recovered = _run_one_cycle(renderer, mock_fetcher, display, stale_result)
    assert len(recovered) == 1
    assert recovered[0].is_stale is False
    assert recovered[0].icao == "DEF456"


def test_recovery_after_empty_returns_fresh(renderer: Renderer) -> None:
    display = NullDisplay()
    last = [_make_aircraft()]
    mock_fetcher = MagicMock()
    # Cycle 1: empty → stale
    mock_fetcher.fetch.return_value = []
    stale_result = _run_one_cycle(renderer, mock_fetcher, display, last)
    assert stale_result[0].is_stale is True
    # Cycle 2: non-empty → recovery
    mock_fetcher.fetch.return_value = [_make_aircraft(icao="GHI789")]
    recovered = _run_one_cycle(renderer, mock_fetcher, display, stale_result)
    assert recovered[0].is_stale is False
