from __future__ import annotations

from datetime import date, datetime
from pathlib import Path

from checkin_cli.weekly_operations_summary import build_weekly_operations_summary
import pytest
from checkin_cli.weekly_operations import (
    CanonicalPin,
    DayState,
    ReminderIdentity,
    WeeklyOperationInput,
    WeeklyOperationsConflict,
    WeeklyOperationsCorruption,
    WeeklyOperationsInputError,
)
from tests._weekly_operations_correlation_support import CUSTOMER, event
from tests._weekly_operations_summary_support import (
    KST,
    SyntheticStatus,
    append_status,
    approved_synthetic_week,
    fixture_for_events,
    sidecar_data_path,
    source_lineage,
)


def test_summary_returns_empty_calendar_week_and_no_prior_comparison(
    tmp_path: Path,
) -> None:
    # Given: a confirmed sidecar whose rows are outside the requested and prior weeks.
    fixture = approved_synthetic_week(tmp_path)
    try:
        # When: a completely empty calendar week is summarized.
        summary = build_weekly_operations_summary(
            fixture.canonical.source, fixture.sidecar.store, date(2026, 8, 3)
        )

        # Then: all seven days remain in the denominator without inventing prior data.
        assert (summary.submitted_count, summary.late_count, summary.missed_count) == (0, 0, 0)
        assert (summary.completed_days, summary.calendar_days) == (0, 7)
        assert summary.adherence_percent == 0.0
        assert summary.prior_week_comparison is None
    finally:
        fixture.close()


def test_summary_counts_partial_week_incidents_without_weight_values(
    tmp_path: Path,
) -> None:
    # Given: a future week has one confirmed missed day and one unaudited reminder.
    fixture = approved_synthetic_week(tmp_path)
    try:
        append_status(
            fixture,
            SyntheticStatus(
                date(2026, 8, 24),
                DayState.MISSED,
                8,
                reminder=ReminderIdentity("summary-incident-01"),
            ),
        )

        # When: the partial seven-calendar-day window is built.
        summary = build_weekly_operations_summary(
            fixture.canonical.source, fixture.sidecar.store, date(2026, 8, 24)
        )

        # Then: the aggregate reports the incident and only a bounded no-data trend.
        assert (summary.submitted_count, summary.late_count, summary.missed_count) == (0, 0, 1)
        assert summary.weight_trend.value == "insufficient_data"
        assert (summary.reminder_sent_count, summary.reminder_incident_count) == (0, 1)
    finally:
        fixture.close()


def test_summary_fails_closed_when_sidecar_data_is_missing(tmp_path: Path) -> None:
    # Given: a registered canonical authority has no durable sidecar history file.
    fixture = fixture_for_events(tmp_path, ())
    try:
        # When/Then: aggregation refuses to reinterpret a missing sidecar as a status.
        with pytest.raises(WeeklyOperationsConflict, match="sidecar is missing"):
            _ = build_weekly_operations_summary(
                fixture.canonical.source, fixture.sidecar.store, date(2026, 8, 17)
            )
    finally:
        fixture.close()


def test_summary_fails_closed_for_non_monday_window(tmp_path: Path) -> None:
    # Given: an otherwise valid canonical and sidecar authority pair.
    fixture = approved_synthetic_week(tmp_path)
    try:
        # When/Then: a Tuesday cannot define a weekly calendar window.
        with pytest.raises(WeeklyOperationsInputError, match="start on Monday"):
            _ = build_weekly_operations_summary(
                fixture.canonical.source, fixture.sidecar.store, date(2026, 8, 18)
            )
    finally:
        fixture.close()


def test_summary_fails_closed_for_canonical_digest_disagreement(
    tmp_path: Path,
) -> None:
    # Given: a self-consistent sidecar row pins an incorrect canonical prefix digest.
    root = event("summary-digest-root", occurred_at="2026-08-17T08:00:00+09:00")
    fixture = fixture_for_events(tmp_path, (root,))
    try:
        _ = fixture.sidecar.store.append(
            WeeklyOperationInput.for_customer(
                CUSTOMER,
                date(2026, 8, 17),
                DayState.SUBMITTED,
                CanonicalPin(1, "0" * 64),
                datetime(2026, 8, 17, 8, 0, tzinfo=KST),
                source_lineage(fixture.canonical, root.event_id),
            )
        )

        # When/Then: the descriptor-bound canonical snapshot rejects the false pin.
        with pytest.raises(WeeklyOperationsConflict, match="canonical digest disagrees"):
            _ = build_weekly_operations_summary(
                fixture.canonical.source, fixture.sidecar.store, date(2026, 8, 17)
            )
    finally:
        fixture.close()


def test_summary_fails_closed_for_stale_canonical_sequence(tmp_path: Path) -> None:
    # Given: a self-consistent sidecar row names a prefix beyond the canonical snapshot.
    root = event("summary-stale-root", occurred_at="2026-08-17T08:00:00+09:00")
    fixture = fixture_for_events(tmp_path, (root,))
    try:
        _ = fixture.sidecar.store.append(
            WeeklyOperationInput.for_customer(
                CUSTOMER,
                date(2026, 8, 17),
                DayState.SUBMITTED,
                CanonicalPin(2, "0" * 64),
                datetime(2026, 8, 17, 8, 0, tzinfo=KST),
                source_lineage(fixture.canonical, root.event_id),
            )
        )

        # When/Then: stale canonical sequence pins never produce a partial aggregate.
        with pytest.raises(WeeklyOperationsConflict, match="canonical pin is stale"):
            _ = build_weekly_operations_summary(
                fixture.canonical.source, fixture.sidecar.store, date(2026, 8, 17)
            )
    finally:
        fixture.close()


def test_summary_fails_closed_for_corrupt_sidecar_bytes(tmp_path: Path) -> None:
    # Given: one real sidecar row is later corrupted in its private durable bytes.
    root = event("summary-corrupt-root", occurred_at="2026-08-17T08:00:00+09:00")
    fixture = fixture_for_events(tmp_path, (root,))
    try:
        append_status(
            fixture,
            SyntheticStatus(date(2026, 8, 17), DayState.SUBMITTED, 1, root.event_id),
        )
        path = sidecar_data_path(fixture)
        corrupt = path.read_bytes().replace(b'"canonical_digest":"', b'"canonical_digest":"f', 1)
        _ = path.write_bytes(corrupt)
        path.chmod(0o600)

        # When/Then: the store rejects corruption before summary serialization.
        with pytest.raises(WeeklyOperationsCorruption):
            _ = build_weekly_operations_summary(
                fixture.canonical.source, fixture.sidecar.store, date(2026, 8, 17)
            )
    finally:
        fixture.close()
