"""Tests for duty-schedule JSON parser."""

import json
from datetime import date

import pytest

from duty_teller.importers.duty_schedule import (
    DUTY_MARKERS,
    MAX_FULL_NAME_LENGTH,
    MAX_SCHEDULE_ROWS,
    UNAVAILABLE_MARKER,
    VACATION_MARKER,
    DutyScheduleParseError,
    parse_duty_schedule,
)


def test_parse_valid_schedule():
    raw = (
        '{"meta": {"start_date": "2026-02-16", "weeks": 2}, '
        '"schedule": ['
        '{"name": "Ivanov I.I.", "duty": "; ; \u0431 ; \u0411 ; \u0432 ; ;"}, '
        '{"name": "Petrov P.P.", "duty": " ; \u041d ; \u041e ; ; ; ;"}'
        "]}"
    ).encode("utf-8")
    result = parse_duty_schedule(raw)
    assert result.start_date == date(2026, 2, 16)
    # Petrov has 7 cells -> end = start + 6
    assert result.end_date == date(2026, 2, 22)
    assert len(result.entries) == 2
    by_name = {e.full_name: e for e in result.entries}
    assert "Ivanov I.I." in by_name
    assert "Petrov P.P." in by_name
    # Ivanov: only duty (б, Б, в) -> 2026-02-18, 19, 20
    ivan = by_name["Ivanov I.I."]
    assert sorted(ivan.duty_dates) == [
        date(2026, 2, 18),
        date(2026, 2, 19),
        date(2026, 2, 20),
    ]
    assert ivan.unavailable_dates == []
    assert ivan.vacation_dates == []
    # Petrov: one Н (unavailable), one О (vacation) -> 2026-02-17, 18
    petr = by_name["Petrov P.P."]
    assert petr.duty_dates == []
    assert petr.unavailable_dates == [date(2026, 2, 17)]
    assert petr.vacation_dates == [date(2026, 2, 18)]


def test_parse_empty_duty_string():
    raw = b'{"meta": {"start_date": "2026-02-01"}, "schedule": [{"name": "A", "duty": ""}]}'
    result = parse_duty_schedule(raw)
    assert result.start_date == date(2026, 2, 1)
    assert result.end_date == date(2026, 2, 1)
    assert len(result.entries) == 1
    assert result.entries[0].full_name == "A"
    assert result.entries[0].duty_dates == []
    assert result.entries[0].unavailable_dates == []
    assert result.entries[0].vacation_dates == []


def test_parse_invalid_json():
    with pytest.raises(DutyScheduleParseError, match="Invalid JSON"):
        parse_duty_schedule(b"not json")


def test_parse_missing_meta():
    with pytest.raises(DutyScheduleParseError, match="meta"):
        parse_duty_schedule(b'{"schedule": []}')


def test_parse_missing_start_date():
    with pytest.raises(DutyScheduleParseError, match="start_date"):
        parse_duty_schedule(b'{"meta": {"weeks": 1}, "schedule": []}')


def test_parse_invalid_start_date():
    with pytest.raises(DutyScheduleParseError, match="start_date|Invalid"):
        parse_duty_schedule(b'{"meta": {"start_date": "not-a-date"}, "schedule": []}')


def test_parse_missing_schedule():
    with pytest.raises(DutyScheduleParseError, match="schedule"):
        parse_duty_schedule(b'{"meta": {"start_date": "2026-02-01"}}')


def test_parse_schedule_not_array():
    with pytest.raises(DutyScheduleParseError, match="schedule"):
        parse_duty_schedule(b'{"meta": {"start_date": "2026-02-01"}, "schedule": {}}')


def test_parse_schedule_item_missing_name():
    with pytest.raises(DutyScheduleParseError, match="name"):
        parse_duty_schedule(
            b'{"meta": {"start_date": "2026-02-01"}, "schedule": [{"duty": ";"}]}'
        )


def test_parse_schedule_item_empty_name():
    with pytest.raises(DutyScheduleParseError, match="empty"):
        parse_duty_schedule(
            b'{"meta": {"start_date": "2026-02-01"}, "schedule": [{"name": "  ", "duty": ""}]}'
        )


def test_duty_markers():
    assert DUTY_MARKERS == frozenset({"б", "Б", "в", "В"})
    assert "Н" not in DUTY_MARKERS and "О" not in DUTY_MARKERS


def test_unavailable_and_vacation_markers():
    assert UNAVAILABLE_MARKER == "Н"
    assert VACATION_MARKER == "О"
    raw = (
        '{"meta": {"start_date": "2026-02-01"}, "schedule": ['
        '{"name": "X", "duty": "\u041d; \u041e; \u0432"}]}'
    ).encode("utf-8")
    result = parse_duty_schedule(raw)
    entry = result.entries[0]
    assert entry.unavailable_dates == [date(2026, 2, 1)]
    assert entry.vacation_dates == [date(2026, 2, 2)]
    assert entry.duty_dates == [date(2026, 2, 3)]


def test_parse_start_date_year_out_of_range():
    """start_date year must be current ± 1; otherwise DutyScheduleParseError."""
    # Use a year far in the past/future so it fails regardless of test run date.
    raw_future = b'{"meta": {"start_date": "2030-01-01"}, "schedule": [{"name": "A", "duty": ""}]}'
    with pytest.raises(DutyScheduleParseError, match="year|2030"):
        parse_duty_schedule(raw_future)
    raw_past = b'{"meta": {"start_date": "2019-01-01"}, "schedule": [{"name": "A", "duty": ""}]}'
    with pytest.raises(DutyScheduleParseError, match="year|2019"):
        parse_duty_schedule(raw_past)


def test_parse_too_many_schedule_rows():
    """More than MAX_SCHEDULE_ROWS rows raises DutyScheduleParseError."""
    rows = [{"name": f"User {i}", "duty": ""} for i in range(MAX_SCHEDULE_ROWS + 1)]
    today = date.today()
    start = today.replace(month=1, day=1)
    payload = {"meta": {"start_date": start.isoformat()}, "schedule": rows}
    raw = json.dumps(payload).encode("utf-8")
    with pytest.raises(DutyScheduleParseError, match="too many|max"):
        parse_duty_schedule(raw)


def test_parse_full_name_too_long():
    """full_name longer than MAX_FULL_NAME_LENGTH raises DutyScheduleParseError."""
    long_name = "A" * (MAX_FULL_NAME_LENGTH + 1)
    today = date.today()
    start = today.replace(month=1, day=1)
    raw = (
        f'{{"meta": {{"start_date": "{start.isoformat()}"}}, '
        f'"schedule": [{{"name": "{long_name}", "duty": ""}}]}}'
    ).encode("utf-8")
    with pytest.raises(DutyScheduleParseError, match="exceed|character"):
        parse_duty_schedule(raw)
