import datetime as dt

import pytest

from schedint.core.storage import OverridesFile, OverrideWindow, Request
from schedint.core.validate import (
    OverlapError,
    _intervals_overlap,
    assert_no_overlap_for_source,
    find_overlaps_for_source,
    request_effective_window,
)

UTC = dt.timezone.utc


def t(y, m, d, h=0):
    return dt.datetime(y, m, d, h, 0, 0, tzinfo=UTC)


def make_req(overrides):
    return Request(
        request_id="c9f6e5a0-5f0c-4a2e-9d9f-3b0d6e6a2a8c",
        source="J1913+0446",
        submitted_at=None,
        submitted_by=None,
        submitted_from_host=None,
        reason=None,
        overrides=overrides,
    )


def make_req_for_overlap(rid: str, source: str, overrides):
    return Request(
        request_id=rid,
        source=source,
        submitted_at=None,
        submitted_by=None,
        submitted_from_host=None,
        reason=None,
        overrides=overrides,
    )


def make_state(*reqs: Request) -> OverridesFile:
    return OverridesFile(
        schema_version=1, file_metadata={}, requests=list(reqs)
    )


def test_simple_overlap():
    assert _intervals_overlap(
        t(2026, 1, 1), t(2026, 1, 5), t(2026, 1, 3), t(2026, 1, 7)
    )


def test_contained_overlap():
    """
    B fully inside A
    """
    assert _intervals_overlap(
        t(2026, 1, 1), t(2026, 1, 10), t(2026, 1, 5), t(2026, 1, 7)
    )


def test_identical_intervals_overlap():
    """
    B fully inside A
    """
    assert _intervals_overlap(
        t(2026, 1, 1), t(2026, 1, 10), t(2026, 1, 1), t(2026, 1, 10)
    )


def test_touching_at_end_is_not_overlap():
    """
    A ends exacty when B starts
    """
    assert not _intervals_overlap(
        t(2026, 1, 1), t(2026, 1, 5), t(2026, 1, 5), t(2026, 1, 10)
    )


def test_touching_at_start_is_not_overlap():
    """
    B ends exacty when A starts
    """
    assert not _intervals_overlap(
        t(2026, 1, 5), t(2026, 1, 10), t(2026, 1, 1), t(2026, 1, 5)
    )


def test_completely_before_no_overlap():
    """
    B starts after A ends
    """
    assert not _intervals_overlap(
        t(2026, 1, 5), t(2026, 1, 10), t(2026, 1, 12), t(2026, 1, 25)
    )


def test_completely_after_no_overlap():
    """
    A starts after B ends
    """
    assert not _intervals_overlap(
        t(2027, 1, 5), t(2027, 1, 10), t(2026, 1, 12), t(2026, 1, 25)
    )


def test_overlap_is_symmetric():
    a_start, a_end = t(2026, 1, 1), t(2026, 1, 5)
    b_start, b_end = t(2026, 1, 3), t(2026, 1, 7)

    assert _intervals_overlap(a_start, a_end, b_start, b_end)
    assert _intervals_overlap(b_start, b_end, a_start, a_end)


def test_effective_window_none_when_no_overrides():
    req = make_req({})
    assert request_effective_window(req) is None


def test_effective_window_ignores_null_value_overrides():
    req = make_req(
        {
            "stars": OverrideWindow(
                value=None, start=t(2026, 1, 1), end=t(2026, 1, 2)
            ),
        }
    )
    assert request_effective_window(req) is None


def test_effective_window_ignores_missing_window_even_if_value_present():
    req = make_req(
        {
            "stars": OverrideWindow(value=3, start=None, end=None),
        }
    )
    assert request_effective_window(req) is None


def test_effective_window_single_override():
    req = make_req(
        {
            "stars": OverrideWindow(
                value=3, start=t(2026, 1, 1), end=t(2026, 1, 2)
            ),
        }
    )
    assert request_effective_window(req) == (t(2026, 1, 1), t(2026, 1, 2))


def test_effective_window_multiple_overrides_uses_min_start_max_end():
    req = make_req(
        {
            "stars": OverrideWindow(
                value=4, start=t(2026, 1, 1), end=t(2026, 1, 2)
            ),
            "tint": OverrideWindow(
                value=4, start=t(2026, 1, 10), end=t(2026, 1, 11)
            ),
            "cadence": OverrideWindow(
                value=None, start=t(2026, 1, 1), end=t(2026, 1, 2)
            ),
        }
    )
    assert request_effective_window(req) == (t(2026, 1, 1), t(2026, 1, 11))


def test_no_overlaps_when_no_requests():
    state = make_state()
    overlaps = find_overlaps_for_source(
        state,
        source="J1913+0446",
        start=t(2026, 1, 1),
        end=t(2026, 1, 2),
    )
    assert overlaps == []


def test_overlap_detected_simple_case():
    state = make_state(
        make_req_for_overlap(
            "req-a",
            "J1913+0446",
            {
                "stars": OverrideWindow(
                    value=4, start=t(2026, 1, 1), end=t(2026, 1, 12)
                ),
                "tint": OverrideWindow(value=None, start=None, end=None),
                "cadence": OverrideWindow(value=None, start=None, end=None),
            },
        )
    )

    overlaps = find_overlaps_for_source(
        state,
        source="J1913+0446",
        start=t(2026, 1, 1),
        end=t(2026, 1, 2),
    )

    assert overlaps == [("req-a", t(2026, 1, 1), t(2026, 1, 12))]


def test_touching_endpoint_is_not_overlap():
    state = make_state(
        make_req_for_overlap(
            "req-a",
            "J1913+0446",
            {
                "stars": OverrideWindow(
                    value=4, start=t(2026, 1, 1), end=t(2026, 1, 12)
                ),
                "tint": OverrideWindow(value=None, start=None, end=None),
                "cadence": OverrideWindow(value=None, start=None, end=None),
            },
        )
    )

    overlaps = find_overlaps_for_source(
        state,
        source="J1913+0446",
        start=t(2026, 1, 12),
        end=t(2026, 1, 24),
    )

    assert overlaps == []


def test_different_source_is_ignored():
    state = make_state(
        make_req_for_overlap(
            "req-a",
            "J1913+0446",
            {
                "stars": OverrideWindow(
                    value=4, start=t(2026, 1, 1), end=t(2026, 1, 12)
                ),
                "tint": OverrideWindow(value=None, start=None, end=None),
                "cadence": OverrideWindow(value=None, start=None, end=None),
            },
        )
    )

    overlaps = find_overlaps_for_source(
        state,
        source="B1937+21",
        start=t(2026, 1, 5),
        end=t(2026, 1, 24),
    )

    assert overlaps == []


def test_request_with_no_effective_window_is_ignored():
    state = make_state(
        make_req_for_overlap(
            "req-a",
            "J1913+0446",
            {
                "stars": OverrideWindow(value=None, start=None, end=None),
                "tint": OverrideWindow(value=None, start=None, end=None),
                "cadence": OverrideWindow(value=None, start=None, end=None),
            },
        )
    )

    overlaps = find_overlaps_for_source(
        state,
        source="J1913+0446",
        start=t(2026, 1, 5),
        end=t(2026, 1, 24),
    )

    assert overlaps == []


def test_effective_window_uses_min_start_max_end_across_overrides():
    state = make_state(
        make_req_for_overlap(
            "req-a",
            "J1913+0446",
            {
                "stars": OverrideWindow(
                    value=4, start=t(2026, 1, 1), end=t(2026, 1, 31)
                ),
                "tint": OverrideWindow(
                    value=720, start=t(2026, 1, 15), end=t(2026, 2, 15)
                ),
            },
        )
    )

    overlaps = find_overlaps_for_source(
        state,
        source="J1913+0446",
        start=t(2026, 1, 19),
        end=t(2026, 1, 24),
    )

    assert overlaps == [("req-a", t(2026, 1, 1), t(2026, 2, 15))]


def test_assert_no_overlap_passes_when_no_request():
    state = make_state()
    assert_no_overlap_for_source(
        state,
        source="B1737-30",
        start=t(2026, 1, 1),
        end=t(2026, 1, 2),
    )


def test_assert_no_overlap_passes_when_no_overlap():
    state = make_state(
        make_req_for_overlap(
            "req-a",
            "J1913+0446",
            {
                "stars": OverrideWindow(
                    value=4, start=t(2026, 1, 1), end=t(2026, 1, 5)
                ),
            },
        )
    )

    assert_no_overlap_for_source(
        state,
        source="J1913+0446",
        start=t(2026, 1, 6),
        end=t(2026, 1, 8),
    )


def test_assert_no_overlap_raises_on_overlap():
    state = make_state(
        make_req_for_overlap(
            "req-a",
            "J1913+0446",
            {
                "stars": OverrideWindow(
                    value=4, start=t(2026, 1, 1), end=t(2026, 1, 5)
                ),
            },
        )
    )

    with pytest.raises(OverlapError) as exc:
        assert_no_overlap_for_source(
            state,
            source="J1913+0446",
            start=t(2026, 1, 4),
            end=t(2026, 1, 8),
        )

    msg = str(exc.value)
    assert "J1913+0446" in msg
    assert "req-a" in msg


def test_assert_no_overlap_ignores_other_sources():
    state = make_state(
        make_req_for_overlap(
            "req-a",
            "J1913+0446",
            {
                "stars": OverrideWindow(
                    value=4, start=t(2026, 1, 1), end=t(2026, 1, 5)
                ),
            },
        )
    )

    assert_no_overlap_for_source(
        state,
        source="B0531+21",
        start=t(2026, 1, 4),
        end=t(2026, 1, 8),
    )
