from __future__ import annotations

import threading
from concurrent.futures import ThreadPoolExecutor
from dataclasses import replace
from datetime import timedelta
from pathlib import Path

import pytest

from gateway.platforms.nutrition_weekly_maintenance_contract import (
    Topic59MaintenanceDecisionKind,
    Topic59MaintenanceSkipAuditV1,
)
from gateway.platforms.nutrition_weekly_maintenance_store import Topic59MaintenanceGate
from tests.gateway.nutrition_weekly_maintenance_support import arm_maintenance


def test_valid_hold_is_consumed_once_and_same_day_is_idempotent(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
    # Given
    fixture = arm_maintenance(tmp_path, monkeypatch)
    gate = Topic59MaintenanceGate.for_profile(fixture.profile)

    # When
    first = gate.evaluate(fixture.projection, fixture.now)
    first_bytes = fixture.audit.read_bytes()
    first_stat = fixture.audit.stat()
    second = gate.evaluate(fixture.projection, fixture.now + timedelta(minutes=20))

    # Then
    assert first.kind is Topic59MaintenanceDecisionKind.NOOP_MAINTENANCE
    assert second == first
    assert fixture.audit.read_bytes() == first_bytes
    assert fixture.audit.stat().st_mtime_ns == first_stat.st_mtime_ns
    assert not (fixture.maintenance / "hold.json").exists()
    assert not (fixture.maintenance / "hold.consuming.json").exists()
    audit = Topic59MaintenanceSkipAuditV1.model_validate_json(first_bytes)
    assert audit.provider_calls == audit.network_calls == 0
    assert audit.sent is audit.delivered is False
    assert audit.message_id is audit.provider_receipt is None


def test_prior_day_terminal_audit_allows_next_day(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
    # Given
    fixture = arm_maintenance(tmp_path, monkeypatch)
    gate = Topic59MaintenanceGate.for_profile(fixture.profile)
    _ = gate.evaluate(fixture.projection, fixture.now)
    tomorrow = replace(
        fixture.projection, kst_day=fixture.projection.kst_day + timedelta(days=1),
    )

    # When
    result = gate.evaluate(tomorrow, fixture.now + timedelta(days=1))

    # Then
    assert result.kind is Topic59MaintenanceDecisionKind.ALLOW


def test_consuming_marker_completes_after_rename_crash(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
    # Given
    fixture = arm_maintenance(tmp_path, monkeypatch)
    consuming = fixture.maintenance / "hold.consuming.json"
    _ = (fixture.maintenance / "hold.json").rename(consuming)

    # When
    result = Topic59MaintenanceGate.for_profile(fixture.profile).evaluate(
        fixture.projection, fixture.now,
    )

    # Then
    assert result.kind is Topic59MaintenanceDecisionKind.NOOP_MAINTENANCE
    assert fixture.audit.is_file()
    assert not consuming.exists()


def test_complete_audit_finalizes_consuming_marker_after_replace_crash(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
    # Given
    fixture = arm_maintenance(tmp_path, monkeypatch)
    gate = Topic59MaintenanceGate.for_profile(fixture.profile)
    first = gate.evaluate(fixture.projection, fixture.now)
    consuming = fixture.maintenance / "hold.consuming.json"
    _ = consuming.write_bytes(fixture.hold_bytes)
    _ = consuming.chmod(0o600)

    # When
    recovered = gate.evaluate(fixture.projection, fixture.now)

    # Then
    assert recovered == first
    assert not consuming.exists()


def test_concurrent_ticks_create_one_audit_without_timing_waits(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
    # Given
    fixture = arm_maintenance(tmp_path, monkeypatch)
    gate = Topic59MaintenanceGate.for_profile(fixture.profile)
    barrier = threading.Barrier(3)

    def evaluate() -> Topic59MaintenanceDecisionKind:
        _ = barrier.wait(timeout=2)
        return gate.evaluate(fixture.projection, fixture.now).kind

    # When
    with ThreadPoolExecutor(max_workers=2) as workers:
        futures = (workers.submit(evaluate), workers.submit(evaluate))
        _ = barrier.wait(timeout=2)
        outcomes = tuple(future.result(timeout=2) for future in futures)

    # Then
    assert outcomes == (
        Topic59MaintenanceDecisionKind.NOOP_MAINTENANCE,
        Topic59MaintenanceDecisionKind.NOOP_MAINTENANCE,
    )
    assert tuple(fixture.maintenance.glob("skip-audit.json")) == (fixture.audit,)
