from __future__ import annotations

import asyncio
from datetime import timedelta
from concurrent.futures import ThreadPoolExecutor

from src import main
from src.config import max_available_date
from src.normals_service import NormalsService
from src.tracking_service import TrackingService, set_tracker


def test_communes_ok(client, monkeypatch) -> None:
    async def fake_search(_q: str):
        return [{"nom": "Paris", "departement": "75", "latitude": 48.8566, "longitude": 2.3522}]

    monkeypatch.setattr(main.commune_service, "search_communes", fake_search)

    response = client.get("/api/communes?q=par")

    assert response.status_code == 200
    body = response.json()
    assert body[0]["nom"] == "Paris"


def test_communes_too_short(client) -> None:
    response = client.get("/api/communes?q=a")
    assert response.status_code == 400


def test_communes_missing_q(client) -> None:
    response = client.get("/api/communes")
    assert response.status_code == 400


def test_weather_ok(client, monkeypatch) -> None:
    async def fake_weather(_lat: float, _lon: float, _start: str, _end: str):
        return {
            "data": [
                {
                    "time": "2024-01-15T00:00",
                    "temperature": 2.1,
                    "precipitation": 0.0,
                    "humidity": 85,
                    "wind_speed": 12.5,
                    "icon": "☁️",
                    "description": "Couvert",
                }
            ],
            "daily_summary": [
                {
                    "date": "2024-01-15",
                    "temp_min": 2.1,
                    "temp_max": 2.1,
                    "precipitation_sum": 0.0,
                    "humidity_avg": 85,
                    "wind_speed_avg": 12.5,
                    "icon": "☁️",
                    "description": "Couvert",
                }
            ],
        }

    monkeypatch.setattr(main.weather_service, "get_weather", fake_weather)

    response = client.get("/api/weather?lat=48.86&lon=2.35&start=2024-01-15&end=2024-01-15")

    assert response.status_code == 200
    assert response.json()["data"][0]["icon"] == "☁️"
    assert response.json()["data"][0]["description"] == "Couvert"
    assert response.json()["daily_summary"][0]["date"] == "2024-01-15"


def test_weather_period_too_long(client) -> None:
    response = client.get("/api/weather?lat=48.86&lon=2.35&start=2024-01-01&end=2024-02-02")
    assert response.status_code == 400
    assert "31 jours" in response.json()["error"]


def test_weather_period_exactly_31_days(client, monkeypatch) -> None:
    async def fake_weather(_lat: float, _lon: float, _start: str, _end: str):
        return {"data": [], "daily_summary": []}

    monkeypatch.setattr(main.weather_service, "get_weather", fake_weather)

    response = client.get("/api/weather?lat=48.86&lon=2.35&start=2024-01-01&end=2024-02-01")
    assert response.status_code == 200


def test_weather_invalid_coordinates(client) -> None:
    response = client.get("/api/weather?lat=100&lon=2.35&start=2024-01-01&end=2024-01-02")
    assert response.status_code == 400
    assert "latitude" in response.json()["error"].lower()


def test_weather_future_date(client) -> None:
    future = (max_available_date() + timedelta(days=1)).isoformat()
    response = client.get(f"/api/weather?lat=48.86&lon=2.35&start={future}&end={future}")
    assert response.status_code == 400
    assert "dates passées" in response.json()["error"]


def test_weather_before_1940(client) -> None:
    response = client.get("/api/weather?lat=48.86&lon=2.35&start=1939-12-31&end=1939-12-31")
    assert response.status_code == 400
    assert "1940" in response.json()["error"]


def test_weather_end_before_start(client) -> None:
    response = client.get("/api/weather?lat=48.86&lon=2.35&start=2024-01-15&end=2024-01-14")
    assert response.status_code == 400
    assert "date de fin" in response.json()["error"]


def test_concurrent_weather_requests(client, monkeypatch) -> None:
    async def fake_weather(lat: float, lon: float, _start: str, _end: str):
        await asyncio.sleep(0.01)
        return {
            "data": [
                {
                    "time": "2024-01-15T00:00",
                    "temperature": lat + lon,
                    "precipitation": 0.0,
                    "humidity": 80,
                    "wind_speed": 10.0,
                    "icon": "☀️",
                    "description": "Dégagé",
                }
            ],
            "daily_summary": [
                {
                    "date": "2024-01-15",
                    "temp_min": 1.0,
                    "temp_max": 3.0,
                    "precipitation_sum": 0.0,
                    "humidity_avg": 80,
                    "wind_speed_avg": 10.0,
                    "icon": "☀️",
                    "description": "Dégagé",
                }
            ],
        }

    monkeypatch.setattr(main.weather_service, "get_weather", fake_weather)

    request1 = "/api/weather?lat=48.86&lon=2.35&start=2024-01-15&end=2024-01-15"
    request2 = "/api/weather?lat=45.76&lon=4.84&start=2024-01-15&end=2024-01-15"

    with ThreadPoolExecutor(max_workers=2) as executor:
        future1 = executor.submit(client.get, request1)
        future2 = executor.submit(client.get, request2)
        response1 = future1.result()
        response2 = future2.result()

    assert response1.status_code == 200
    assert response2.status_code == 200
    assert response1.json()["data"]
    assert response2.json()["data"]


def test_resolve_api_route(client, monkeypatch) -> None:
    async def fake_resolve(_slug: str):
        return {
            "nom": "Paris",
            "departement": "75",
            "latitude": 48.8566,
            "longitude": 2.3522,
            "slug": "paris-75",
        }

    monkeypatch.setattr(main.commune_service, "resolve_slug", fake_resolve)

    response = client.get("/api/resolve/paris-75")

    assert response.status_code == 200
    assert response.json()["nom"] == "Paris"


def test_resolve_api_route_404(client, monkeypatch) -> None:
    async def fake_resolve(_slug: str):
        return None

    monkeypatch.setattr(main.commune_service, "resolve_slug", fake_resolve)

    response = client.get("/api/resolve/xyz-99")

    assert response.status_code == 404


def test_normals_api_route_ok(client, monkeypatch) -> None:
    async def fake_normals(_lat: float, _lon: float, _start: str, _end: str):
        return {
            "elevation": 608.0,
            "reference_period": "1991-2020",
            "period_normals": {
                "temp_avg": 10.4,
                "temp_max_avg": 14.2,
                "temp_min_avg": 6.8,
                "precipitation_daily_avg": 2.1,
                "precipitation_total": 14.7,
            },
            "daily_normals": [
                {
                    "month_day": "03-05",
                    "temp_avg": 9.8,
                    "temp_max": 13.5,
                    "temp_min": 6.2,
                    "precipitation": 1.9,
                }
            ],
            "month_normals": {
                "month": 3,
                "month_name": "mars",
                "temp_avg": 10.2,
                "temp_max_avg": 14.5,
                "temp_min_avg": 6.1,
                "precipitation_total": 62.3,
            },
        }

    monkeypatch.setattr(main.normals_service, "get_normals", fake_normals)

    response = client.get(
        "/api/normals?lat=48.86&lon=2.35&start=2024-03-05&end=2024-03-11"
    )

    assert response.status_code == 200
    assert response.json()["reference_period"] == "1991-2020"
    assert response.json()["month_normals"]["month"] == 3


def test_normals_api_route_missing_params(client) -> None:
    response = client.get("/api/normals")

    assert response.status_code == 400


def test_api_annual_normals_endpoint(client, monkeypatch) -> None:
    async def fake_annual_normals(_lat: float, _lon: float):
        return {
            "elevation": 35.0,
            "reference_period": "1991-2020",
            "annual_avg_temp": 12.3,
            "annual_precipitation": 740.5,
            "months": [
                {
                    "month": index,
                    "month_name": name,
                    "temp_avg": 10.0,
                    "temp_max_avg": 14.0,
                    "temp_min_avg": 6.0,
                    "precipitation_total": 50.0,
                }
                for index, name in enumerate(
                    [
                        "",
                        "janvier",
                        "février",
                        "mars",
                        "avril",
                        "mai",
                        "juin",
                        "juillet",
                        "août",
                        "septembre",
                        "octobre",
                        "novembre",
                        "décembre",
                    ]
                )
                if index
            ],
        }

    monkeypatch.setattr(main.normals_service, "get_annual_normals", fake_annual_normals)

    response = client.get("/api/normals/annual?lat=48.85&lon=2.35")

    assert response.status_code == 200
    assert len(response.json()["months"]) == 12


def test_api_annual_normals_missing_params(client) -> None:
    response = client.get("/api/normals/annual")

    assert response.status_code == 400


def test_normals_api_route_invalid_coords(client) -> None:
    response = client.get(
        "/api/normals?lat=999&lon=2.35&start=2024-03-05&end=2024-03-11"
    )

    assert response.status_code == 400
    assert "latitude" in response.json()["error"].lower()


def test_normals_api_route_upstream_error(client, monkeypatch) -> None:
    async def fake_normals(_lat: float, _lon: float, _start: str, _end: str):
        raise main.NormalsUpstreamError

    monkeypatch.setattr(main.normals_service, "get_normals", fake_normals)

    response = client.get(
        "/api/normals?lat=48.86&lon=2.35&start=2024-03-05&end=2024-03-11"
    )

    assert response.status_code == 502
    assert "normales climatiques" in response.json()["error"].lower()


def test_seo_route_meteo(client) -> None:
    response = client.get("/meteo/paris-75/2024-01-15/2024-01-15")

    assert response.status_code == 200
    assert "text/html" in response.headers.get("content-type", "")


def test_seo_meteo_page_contains_prefetched_data(client, monkeypatch) -> None:
    async def fake_prefetch(*_args, **_kwargs):
        return {
            "commune": {"nom": "Paris", "latitude": 48.8566, "longitude": 2.3522},
            "weather": {"data": [{"time": "2024-01-15T00:00"}], "daily_summary": []},
            "normals": None,
        }

    monkeypatch.setattr(main, "prefetch_period", fake_prefetch)

    response = client.get("/meteo/paris-75/2024-01-15/2024-01-15")

    assert response.status_code == 200
    assert '<script id="prefetched-data" type="application/json">' in response.text


def test_seo_meteo_page_without_prefetch_on_failure(client, monkeypatch) -> None:
    async def fake_prefetch(*_args, **_kwargs):
        return None

    monkeypatch.setattr(main, "prefetch_period", fake_prefetch)

    response = client.get("/meteo/paris-75/2024-01-15/2024-01-15")

    assert response.status_code == 200
    assert 'id="prefetched-data"' not in response.text


def test_seo_ville_page_contains_prefetched_data(client, monkeypatch) -> None:
    async def fake_resolve(_slug: str):
        return {
            "nom": "Paris",
            "departement": "75",
            "latitude": 48.8566,
            "longitude": 2.3522,
        }

    async def fake_prefetch(*_args, **_kwargs):
        return {
            "commune": {"nom": "Paris", "latitude": 48.8566, "longitude": 2.3522},
            "annual_climate": {"annual_avg_temp": 12.3, "months": []},
        }

    monkeypatch.setattr(main.commune_service, "resolve_slug", fake_resolve)
    monkeypatch.setattr(main, "prefetch_town", fake_prefetch)

    response = client.get("/ville/paris-75")

    assert response.status_code == 200
    assert '<script id="prefetched-data" type="application/json">' in response.text


def test_seo_month_page_contains_prefetched_data(client, monkeypatch) -> None:
    async def fake_resolve(_slug: str):
        return {
            "nom": "Paris",
            "departement": "75",
            "latitude": 48.8566,
            "longitude": 2.3522,
        }

    async def fake_prefetch(*_args, **_kwargs):
        return {
            "commune": {"nom": "Paris", "latitude": 48.8566, "longitude": 2.3522},
            "weather": {"data": [{"time": "2026-03-01T00:00"}], "daily_summary": []},
            "normals": None,
        }

    monkeypatch.setattr(main.commune_service, "resolve_slug", fake_resolve)
    monkeypatch.setattr(main, "prefetch_month", fake_prefetch)

    response = client.get("/meteo/paris-75/2026/03")

    assert response.status_code == 200
    assert '<script id="prefetched-data" type="application/json">' in response.text


def test_inject_prefetched_data_escapes_script_tag() -> None:
    html = "<html><head><title>HistoMeteo</title></head><body></body></html>"
    payload = {"commune": {"nom": "x</script><script>alert(1)</script>"}}

    injected = main._inject_prefetched_data(html, payload)

    assert "<\\/script>" in injected
    assert "id=\"prefetched-data\"" in injected


def test_seo_ville_page_og_tags(client, monkeypatch) -> None:
    async def fake_resolve(_slug: str):
        return {
            "nom": "Paris",
            "departement": "75",
            "latitude": 48.8566,
            "longitude": 2.3522,
        }

    monkeypatch.setattr(main.commune_service, "resolve_slug", fake_resolve)

    response = client.get("/ville/paris-75")

    assert response.status_code == 200
    assert 'property="og:title" content="Météo et climat à Paris"' in response.text


def test_seo_month_page_og_tags(client, monkeypatch) -> None:
    async def fake_resolve(_slug: str):
        return {
            "nom": "Paris",
            "departement": "75",
            "latitude": 48.8566,
            "longitude": 2.3522,
        }

    monkeypatch.setattr(main.commune_service, "resolve_slug", fake_resolve)

    response = client.get("/meteo/paris-75/2026/03")

    assert response.status_code == 200
    assert 'property="og:title" content="Météo à Paris en mars 2026"' in response.text


def test_seo_month_page_invalid_month(client) -> None:
    response = client.get("/meteo/paris-75/2026/13")

    assert response.status_code == 200
    assert "Météo à Paris en" not in response.text


def test_seo_month_page_future(client) -> None:
    response = client.get("/meteo/paris-75/2030/06")

    assert response.status_code == 200
    assert "Météo à Paris en" not in response.text


def test_seo_period_route_still_works(client) -> None:
    response = client.get("/meteo/paris-75/2026-03-01/2026-03-07")

    assert response.status_code == 200
    assert "Météo à Paris du 1er au 7 mars 2026" in response.text


def test_seo_route_comparaison(client) -> None:
    response = client.get(
        "/comparaison/paris-75/vs/lyon-69/2024-01-15/2024-01-15"
    )

    assert response.status_code == 200
    assert "text/html" in response.headers.get("content-type", "")


def test_legacy_redirect_simple(client) -> None:
    response = client.get(
        "/?commune=Paris&dept=75&lat=48.86&lon=2.35&start=2024-01-15&end=2024-01-15",
        follow_redirects=False,
    )

    assert response.status_code == 301
    assert response.headers["location"] == "/meteo/paris-75/2024-01-15/2024-01-15"


def test_legacy_redirect_comparison(client) -> None:
    response = client.get(
        "/?commune=Paris&dept=75&lat=48.86&lon=2.35&start=2024-01-15&end=2024-01-15&commune2=Lyon&dept2=69&lat2=45.76&lon2=4.84",
        follow_redirects=False,
    )

    assert response.status_code == 301
    assert (
        response.headers["location"]
        == "/comparaison/paris-75/vs/lyon-69/2024-01-15/2024-01-15"
    )


def test_homepage_no_redirect(client) -> None:
    response = client.get("/", follow_redirects=False)

    assert response.status_code == 200
    assert "text/html" in response.headers.get("content-type", "")


def test_legacy_redirect_invalid_dept(client) -> None:
    response = client.get(
        "/?commune=Paris&dept=EVIL&start=2024-01-15&end=2024-01-15",
        follow_redirects=False,
    )

    assert response.status_code == 200
    assert "text/html" in response.headers.get("content-type", "")


def test_seo_page_contains_dynamic_og_title(client) -> None:
    response = client.get("/meteo/bordeaux-33/2026-03-09/2026-03-11")

    assert response.status_code == 200
    assert 'property="og:title"' in response.text
    assert "Météo à Bordeaux du 9 au 11 mars 2026" in response.text


def test_seo_page_contains_twitter_card(client) -> None:
    response = client.get("/meteo/bordeaux-33/2026-03-09/2026-03-11")

    assert response.status_code == 200
    assert 'name="twitter:card" content="summary_large_image"' in response.text


def test_seo_page_contains_canonical(client) -> None:
    response = client.get("/meteo/bordeaux-33/2026-03-09/2026-03-11")

    assert response.status_code == 200
    assert (
        '<link rel="canonical" href="/meteo/bordeaux-33/2026-03-09/2026-03-11"'
        in response.text
    )


def test_og_image_endpoint_returns_png(client, monkeypatch) -> None:
    async def fake_resolve(_slug: str):
        return {
            "nom": "Bordeaux",
            "departement": "33",
            "latitude": 44.8378,
            "longitude": -0.5792,
        }

    async def fake_weather(_lat: float, _lon: float, _start: str, _end: str):
        return {
            "daily_summary": [
                {"temp_min": 2.0, "temp_max": 12.0, "description": "Couvert"},
                {"temp_min": 4.0, "temp_max": 15.0, "description": "Couvert"},
            ]
        }

    monkeypatch.setattr(main.commune_service, "resolve_slug", fake_resolve)
    monkeypatch.setattr(main.weather_service, "get_weather", fake_weather)

    response = client.get("/api/og-image/bordeaux-33/2026-03-09/2026-03-11")

    assert response.status_code == 200
    assert response.headers.get("content-type") == "image/png"
    assert response.content.startswith(b"\x89PNG")


def test_og_image_endpoint_invalid_dates(client) -> None:
    response = client.get("/api/og-image/bordeaux-33/invalid/invalid")
    assert response.status_code == 422


def test_homepage_keeps_default_og(client) -> None:
    response = client.get("/")

    assert response.status_code == 200
    assert 'property="og:title" content="HistoMétéo — Historique de la Météo"' in response.text


def test_weather_receives_commune_slug(client, monkeypatch, tmp_path) -> None:
    tracker = TrackingService(tmp_path / "tracking.db")
    set_tracker(tracker)

    async def fake_weather(_lat: float, _lon: float, _start: str, _end: str):
        return {"data": [], "daily_summary": []}

    monkeypatch.setattr(main.weather_service, "get_weather", fake_weather)

    response = client.get(
        "/api/weather?lat=44&lon=6&start=2026-03-01&end=2026-03-02&commune=Tallard&slug=tallard-05"
    )
    row = tracker.conn.execute(
        "SELECT commune_1, commune_1_slug FROM search_logs ORDER BY created_at DESC LIMIT 1"
    ).fetchone()

    assert response.status_code == 200
    assert row is not None
    assert row["commune_1"] == "Tallard"
    assert row["commune_1_slug"] == "tallard-05"

    tracker.close()
    set_tracker(None)


def test_normals_accepts_search_id(client, monkeypatch, tmp_path) -> None:
    tracker = TrackingService(tmp_path / "tracking.db")
    set_tracker(tracker)

    class NormalsResponse:
        def __init__(self) -> None:
            self.status_code = 200

        def raise_for_status(self) -> None:
            return None

        def json(self):
            return {
                "elevation": 35,
                "daily": {
                    "time": ["1991-01-01"],
                    "temperature_2m_mean": [10.0],
                    "temperature_2m_max": [12.0],
                    "temperature_2m_min": [8.0],
                    "precipitation_sum": [1.0],
                },
            }

    class NormalsClient:
        async def get(self, *_args, **_kwargs):
            return NormalsResponse()

        async def aclose(self) -> None:
            return None

    monkeypatch.setattr(main, "normals_service", NormalsService(client=NormalsClient()))
    search_id = "11111111-1111-4111-8111-111111111111"

    response = client.get(
        f"/api/normals?lat=44&lon=6&start=2026-03-01&end=2026-03-02&search_id={search_id}"
    )
    rows = tracker.conn.execute(
        "SELECT search_id FROM api_call_logs WHERE service = 'normals'"
    ).fetchall()

    assert response.status_code == 200
    assert len(rows) == 3
    assert all(row["search_id"] == search_id for row in rows)

    tracker.close()
    set_tracker(None)


def test_normals_annual_accepts_search_id(client, monkeypatch, tmp_path) -> None:
    tracker = TrackingService(tmp_path / "tracking.db")
    set_tracker(tracker)

    class NormalsResponse:
        def __init__(self) -> None:
            self.status_code = 200

        def raise_for_status(self) -> None:
            return None

        def json(self):
            return {
                "elevation": 35,
                "daily": {
                    "time": ["1991-01-01"],
                    "temperature_2m_mean": [10.0],
                    "temperature_2m_max": [12.0],
                    "temperature_2m_min": [8.0],
                    "precipitation_sum": [1.0],
                },
            }

    class NormalsClient:
        async def get(self, *_args, **_kwargs):
            return NormalsResponse()

        async def aclose(self) -> None:
            return None

    monkeypatch.setattr(main, "normals_service", NormalsService(client=NormalsClient()))
    search_id = "22222222-2222-4222-8222-222222222222"

    response = client.get(
        f"/api/normals/annual?lat=44&lon=6&search_id={search_id}"
    )
    rows = tracker.conn.execute(
        "SELECT search_id FROM api_call_logs WHERE service = 'normals'"
    ).fetchall()

    assert response.status_code == 200
    assert len(rows) == 3
    assert all(row["search_id"] == search_id for row in rows)

    tracker.close()
    set_tracker(None)


def test_normals_rejects_invalid_search_id(client, monkeypatch, tmp_path) -> None:
    tracker = TrackingService(tmp_path / "tracking.db")
    set_tracker(tracker)

    class NormalsResponse:
        def __init__(self) -> None:
            self.status_code = 200

        def raise_for_status(self) -> None:
            return None

        def json(self):
            return {
                "elevation": 35,
                "daily": {
                    "time": ["1991-01-01"],
                    "temperature_2m_mean": [10.0],
                    "temperature_2m_max": [12.0],
                    "temperature_2m_min": [8.0],
                    "precipitation_sum": [1.0],
                },
            }

    class NormalsClient:
        async def get(self, *_args, **_kwargs):
            return NormalsResponse()

        async def aclose(self) -> None:
            return None

    monkeypatch.setattr(main, "normals_service", NormalsService(client=NormalsClient()))

    response = client.get(
        "/api/normals?lat=44&lon=6&start=2026-03-01&end=2026-03-02&search_id=not-a-uuid"
    )
    rows = tracker.conn.execute(
        "SELECT search_id FROM api_call_logs WHERE service = 'normals'"
    ).fetchall()

    assert response.status_code == 200
    assert len(rows) == 3
    assert all(row["search_id"] is None for row in rows)

    tracker.close()
    set_tracker(None)


def test_search_total_includes_normals(client, monkeypatch, tmp_path) -> None:
    tracker = TrackingService(tmp_path / "tracking.db")
    set_tracker(tracker)

    class NormalsResponse:
        def __init__(self) -> None:
            self.status_code = 200

        def raise_for_status(self) -> None:
            return None

        def json(self):
            return {
                "elevation": 35,
                "daily": {
                    "time": ["1991-01-01"],
                    "temperature_2m_mean": [10.0],
                    "temperature_2m_max": [12.0],
                    "temperature_2m_min": [8.0],
                    "precipitation_sum": [1.0],
                },
            }

    class NormalsClient:
        async def get(self, *_args, **_kwargs):
            return NormalsResponse()

        async def aclose(self) -> None:
            return None

    class WeatherResponse:
        def __init__(self) -> None:
            self.status_code = 200

        def raise_for_status(self) -> None:
            return None

        def json(self):
            return {
                "hourly": {
                    "time": ["2026-03-01T00:00"],
                    "temperature_2m": [10.0],
                    "precipitation": [0.0],
                    "relative_humidity_2m": [70],
                    "wind_speed_10m": [5.0],
                    "weather_code": [0],
                }
            }

    class WeatherClient:
        async def get(self, *_args, **_kwargs):
            return WeatherResponse()

        async def aclose(self) -> None:
            return None

    monkeypatch.setattr(main, "normals_service", NormalsService(client=NormalsClient()))
    monkeypatch.setattr(main, "weather_service", main.WeatherService(client=WeatherClient()))

    search_id = "33333333-3333-4333-8333-333333333333"
    normals_response = client.get(
        f"/api/normals?lat=44&lon=6&start=2026-03-01&end=2026-03-02&search_id={search_id}"
    )
    weather_response = client.get(
        f"/api/weather?lat=44&lon=6&start=2026-03-01&end=2026-03-02&commune=Tallard&slug=tallard-05&search_id={search_id}"
    )
    row = tracker.conn.execute(
        "SELECT total_api_calls FROM search_logs WHERE id = ?",
        (search_id,),
    ).fetchone()

    assert normals_response.status_code == 200
    assert weather_response.status_code == 200
    assert row is not None
    assert row["total_api_calls"] == 4

    tracker.close()
    set_tracker(None)
