import json
import sys
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import patch

import pytest
from py_toon_format import decode

PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

import main  # noqa: E402


@pytest.fixture(autouse=True)
def reset_config() -> None:
    """Reset server config after each test to avoid side effects."""
    yield
    main.config.output_timezone = None


SAMPLE_EVENTS = [
    {
        "uid": "evt-1",
        "summary": "ABC123, Förel",
        "description": "Applied Biology",
        "location": "Lund Room A",
        "url": "",
        "start": "2026-03-01T08:00:00+00:00",
        "end": "2026-03-01T10:00:00+00:00",
        "start_date": "2026-03-01",
        "course_code": "ABC123",
        "course_codes": ["ABC123"],
        "groups": ["D3.07"],
        "course_names": ["Applied Biology"],
        "programs": [{"id": "111111", "name": "Program Alpha"}],
        "search_blob": "abc123 d3.07 förel lecture applied biology lund room a program alpha",
    },
    {
        "uid": "evt-2",
        "summary": "XYZ101, Lab",
        "description": "Data Systems",
        "location": "Malmö Lab",
        "url": "",
        "start": "2026-03-05T09:00:00+00:00",
        "end": "2026-03-05T12:00:00+00:00",
        "start_date": "2026-03-05",
        "course_code": "XYZ101",
        "course_codes": ["XYZ101"],
        "groups": ["C3.06"],
        "course_names": ["Data Systems"],
        "programs": [{"id": "222222", "name": "Program Beta"}],
        "search_blob": "xyz101 c3.06 lab data systems malmö program beta",
    },
    {
        "uid": "evt-3",
        "summary": "ABC123, Seminar",
        "description": "Applied Biology",
        "location": "Lund Room B",
        "url": "",
        "start": "2026-03-10T08:00:00+00:00",
        "end": "2026-03-10T10:00:00+00:00",
        "start_date": "2026-03-10",
        "course_code": "ABC123",
        "course_codes": ["ABC123"],
        "groups": ["D3.08"],
        "course_names": ["Applied Biology"],
        "programs": [{"id": "111111", "name": "Program Alpha"}],
        "search_blob": "abc123 d3.08 seminar applied biology lund room b program alpha",
    },
]


def _mock_index() -> dict:
    return {"events": SAMPLE_EVENTS}


def _decode_rows(toon_output: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
    payload = decode(toon_output)
    columns = payload["columns"]

    def _decode_cell(value: Any) -> Any:
        if isinstance(value, str) and value[:1] in "[{":
            try:
                return json.loads(value)
            except json.JSONDecodeError:
                return value
        return value

    rows = [
        dict(zip(columns, (_decode_cell(cell) for cell in row), strict=False))
        for row in payload["rows"]
    ]
    return payload, rows


def test_validate_index_payload_accepts_valid_data() -> None:
    payload = {
        "cache_schema_version": main.CACHE_SCHEMA_VERSION,
        "source_url": "https://example.com/calendar.ics",
        "fetched_at": "2026-03-20T00:00:00+00:00",
        "events": [SAMPLE_EVENTS[0]],
    }

    validated = main._validate_index_payload(payload)
    assert validated["source_url"] == payload["source_url"]
    assert validated["events"][0]["uid"] == "evt-1"


def test_read_cache_rejects_invalid_typed_payload() -> None:
    invalid_payload = {
        "cache_schema_version": main.CACHE_SCHEMA_VERSION,
        "source_url": "https://example.com/calendar.ics",
        "fetched_at": "2026-03-20T00:00:00+00:00",
        "events": [{"uid": 123}],
    }

    with tempfile.TemporaryDirectory() as tmp_dir:
        path = Path(tmp_dir) / "cache.json"
        path.write_text(json.dumps(invalid_payload), encoding="utf-8")

        cached = main._read_cache(path, "https://example.com/calendar.ics")
        assert cached is None


@patch("main._load_index", side_effect=_mock_index)
def test_list_programs(_mock_load_index: Any) -> None:
    payload, rows = _decode_rows(main.list_programs(include_program_id=True))
    assert payload["dataset"] == "programs"
    assert len(rows) == 2

    alpha = next(item for item in rows if item["id"] == "111111")
    beta = next(item for item in rows if item["id"] == "222222")

    assert alpha["name"] == "Program Alpha"
    assert alpha["event_count"] == 2
    assert alpha["course_count"] == 2

    assert beta["name"] == "Program Beta"
    assert beta["event_count"] == 1
    assert beta["course_count"] == 2


@patch("main._load_index", side_effect=_mock_index)
def test_list_courses_for_program(_mock_load_index: Any) -> None:
    payload, rows = _decode_rows(main.list_courses_for_program("Program Alpha"))
    assert payload["dataset"] == "courses"
    assert len(rows) == 1

    course = rows[0]
    assert course["course"] == "Applied Biology"
    assert course["codes"] == ["ABC123"]
    assert course["programs"] == ["Program Alpha"]
    assert course["event_count"] == 2


def test_list_courses_for_program_rejects_empty_query() -> None:
    with pytest.raises(ValueError, match="program_query cannot be empty"):
        main.list_courses_for_program("   ")


@patch("main._load_index", side_effect=_mock_index)
def test_search_courses_fuzzy(_mock_load_index: Any) -> None:
    payload, rows = _decode_rows(main.search_courses_fuzzy("biology", limit=5))
    assert payload["dataset"] == "course_matches"
    assert len(rows) >= 1

    top = rows[0]
    assert top["course"] == "Applied Biology"
    assert "ABC123" in top["codes"]
    assert top["score"] >= 0.45


def test_search_courses_fuzzy_rejects_empty_query() -> None:
    with pytest.raises(ValueError, match="query cannot be empty"):
        main.search_courses_fuzzy(" ")


@patch("main._load_index", side_effect=_mock_index)
def test_list_events_for_course(_mock_load_index: Any) -> None:
    main.config.output_timezone = "Europe/Stockholm"
    _, rows = _decode_rows(
        main.list_events_for_course(
            "ABC123",
            start_date="2026-03-01",
            end_date="2026-03-03",
            include_uid=True,
        )
    )
    assert len(rows) == 1
    assert rows[0]["uid"] == "evt-1"
    assert rows[0]["start"].endswith("+01:00")
    assert rows[0]["end"].endswith("+01:00")
    assert rows[0]["course_code"] == "ABC123"
    assert rows[0]["groups"] == ["D3.07"]


@patch("main._load_index", side_effect=_mock_index)
def test_list_events_for_course_output_timezone_manual(_mock_load_index: Any) -> None:
    main.config.output_timezone = "UTC"
    _, rows = _decode_rows(
        main.list_events_for_course(
            "ABC123",
            start_date="2026-03-01",
            end_date="2026-03-03",
            include_uid=True,
        )
    )
    assert len(rows) == 1
    assert rows[0]["start"].endswith("+00:00")


@patch("main._load_index", side_effect=_mock_index)
def test_list_events_for_course_rejects_invalid_timezone(_mock_load_index: Any) -> None:
    main.config.output_timezone = "Mars/Phobos"
    with pytest.raises(ValueError, match="Invalid output_timezone"):
        main.list_events_for_course("ABC123")


def test_list_events_for_course_rejects_invalid_date() -> None:
    with pytest.raises(ValueError, match="Invalid date format"):
        main.list_events_for_course("ABC123", start_date="2026/03/01")


@patch("main._load_index", side_effect=_mock_index)
def test_list_events_for_program(_mock_load_index: Any) -> None:
    main.config.output_timezone = "Europe/Stockholm"
    _, rows = _decode_rows(
        main.list_events_for_program(
            "111111",
            start_date="2026-03-01",
            end_date="2026-03-31",
            include_uid=True,
        )
    )
    assert len(rows) == 2
    assert {item["uid"] for item in rows} == {"evt-1", "evt-3"}


def test_list_events_for_program_rejects_empty_query() -> None:
    with pytest.raises(ValueError, match="program_query cannot be empty"):
        main.list_events_for_program("   ")


@patch("main._load_index", side_effect=_mock_index)
def test_list_events_filtered(_mock_load_index: Any) -> None:
    main.config.output_timezone = "Europe/Stockholm"
    _, rows = _decode_rows(
        main.list_events_filtered(
            text_query="förel",
            location_query="lund room a",
            course_query="ABC123",
            program_query="Program Alpha",
            start_date="2026-03-01",
            end_date="2026-03-02",
            limit=10,
            include_uid=True,
        )
    )
    assert len(rows) == 1
    assert rows[0]["uid"] == "evt-1"


@patch("main._load_index", side_effect=_mock_index)
def test_list_events_filtered_by_group(_mock_load_index: Any) -> None:
    main.config.output_timezone = "Europe/Stockholm"
    _, rows = _decode_rows(
        main.list_events_filtered(
            group_query="D3.07",
            start_date="2026-03-01",
            end_date="2026-03-31",
            include_uid=True,
        )
    )
    assert len(rows) == 1
    assert rows[0]["uid"] == "evt-1"


@patch("main._load_index", side_effect=_mock_index)
def test_default_response_is_toon(_mock_load_index: Any) -> None:
    result = main.list_programs()
    assert isinstance(result, str)

    decoded = decode(result)
    assert decoded["dataset"] == "programs"
    assert decoded["columns"] == ["name", "event_count", "course_count"]
    assert len(decoded["rows"]) == 2


def test_list_events_filtered_requires_non_date_filter() -> None:
    with pytest.raises(ValueError, match="At least one filter is required"):
        main.list_events_filtered(start_date="2026-03-01", end_date="2026-03-02")


def test_extract_course_and_group_codes() -> None:
    course_codes, groups = main._extract_course_and_group_codes(
        "EITG20, Övn D3.07",
        ["Studentgrupp: D3.07, E4-KS", "Program: Signalbehandling"],
    )

    assert course_codes == ["EITG20"]
    assert groups == ["D3.07", "E4-KS"]


def test_extract_event_type_from_summary() -> None:
    event_type = main._extract_event_type("EITG20, Förel", [])
    assert event_type == "other"
