"""Strict fixtures for adversarial Topic-59 maintenance tests."""

from __future__ import annotations

import hashlib
import os
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Final
from zoneinfo import ZoneInfo

import pytest

from gateway.platforms.nutrition_weekly_maintenance_store import Topic59MaintenanceGate
from gateway.platforms.nutrition_weekly_maintenance_contract import (
    Topic59MaintenanceAuthorityV1,
    Topic59MaintenanceHoldV1,
    Topic59MaintenanceScopeV1,
    canonical_document,
    derive_hold_id,
    maintenance_scope_digest,
)
from gateway.platforms.nutrition_weekly_operations import (
    Topic59DayCardProjector, Topic59PublicationRequest, Topic59PublicationResult,
)
from gateway.platforms.nutrition_weekly_operations_authority import (
    WeeklyOperationsRuntimeContext,
)
from gateway.platforms.nutrition_weekly_operations_config import (
    WeeklyOperationsConfig, WeeklyOperationsReviewRoute,
)
from gateway.platforms.nutrition_weekly_operations_publication_contract import (
    Topic59LedgerAction, Topic59LedgerState, Topic59ProviderOutcome,
)
from gateway.platforms.nutrition_weekly_operations_models import (
    Topic59ApprovalState, Topic59DayCard, Topic59DayState, Topic59DeliveryState,
    Topic59DraftState, Topic59Projection, project_topic59_day_card,
)
from tests.gateway._weekly_operations_authority_support import (
    authority_receipt_fixture, test_registry_identity,
)

KST: Final = ZoneInfo("Asia/Seoul")
NOW: Final = datetime(2031, 2, 3, 9, 0, tzinfo=KST)
CANDIDATE: Final = "a" * 64
CONFIG: Final = "e" * 64
ROUTE: Final = "f" * 64
CUSTOMER_KEY: Final = "client_001"
CUSTOMER: Final = hashlib.sha256(
    f"nutricoach-weekly-operations-customer-identity-v1\0{CUSTOMER_KEY}".encode(),
).hexdigest()
CONSENT: Final = "6" * 64


@dataclass(frozen=True, slots=True)
class MaintenanceFixture:
    profile: Path
    credentials: Path
    maintenance: Path
    credential: Path
    hold: Topic59MaintenanceHoldV1
    authority: Topic59MaintenanceAuthorityV1
    hold_bytes: bytes
    projection: Topic59Projection
    request: Topic59PublicationRequest
    now: datetime

    @property
    def audit(self) -> Path:
        return self.maintenance / "skip-audit.json"

    @property
    def ledger(self) -> Path:
        return self.profile / "data" / "weekly-operations-topic59.jsonl"


@dataclass(frozen=True, slots=True)
class _SidecarRow:
    customer_identity_digest: str
    kst_day: date
    state: str
    source_event_id: str | None
    logical_key: str
    row_digest: str


def publication_request(now: datetime = NOW) -> Topic59PublicationRequest:
    config = WeeklyOperationsConfig(
        True,
        review_route=WeeklyOperationsReviewRoute("90001", "10001"),
        registry_identity_binding_digest=test_registry_identity().binding_digest,
    )
    receipt = authority_receipt_fixture(
        CANDIDATE, config.digest, (CUSTOMER_KEY,), ("90001", "10001", 1),
        CONSENT, now - timedelta(days=1), now + timedelta(days=1),
        config.feature_epoch,
    )
    runtime = WeeklyOperationsRuntimeContext(
        CANDIDATE, CUSTOMER_KEY, "90001", "10001", 1, CONSENT, True,
        config.feature_epoch, now,
    )
    row = _SidecarRow(
        customer_identity_digest=CUSTOMER,
        kst_day=now.date(),
        state=Topic59DayState.SUBMITTED.value,
        source_event_id="event-maintenance",
        logical_key="row-maintenance",
        row_digest="7" * 64,
    )
    card = Topic59DayCard.from_sidecar(
        row, completed_field_count=5, draft_state=Topic59DraftState.DRAFTED,
        approval_state=Topic59ApprovalState.PENDING,
        delivery_state=Topic59DeliveryState.NOT_SENT,
        candidate_digest=CANDIDATE,
    )
    return Topic59PublicationRequest(card, config, receipt, runtime)


def arm_maintenance(
    root: Path, monkeypatch: pytest.MonkeyPatch, *, now: datetime = NOW,
) -> MaintenanceFixture:
    profile = root / "profile"
    credentials = root / "credentials"
    profile.mkdir(mode=0o700)
    request = publication_request(now)
    route = request.config.review_route
    if route is None:
        raise AssertionError
    exact_projection = project_topic59_day_card(
        request.card, config_digest=request.config.digest, route_key=route.key,
    )
    scope = Topic59MaintenanceScopeV1(
        candidate_digest=exact_projection.candidate_digest,
        config_digest=exact_projection.config_digest,
        route_digest=exact_projection.route_digest,
        customer_identity_digest=exact_projection.customer_identity_digest,
        card_slot=exact_projection.card_slot,
        kst_day=now.date(),
        not_before=now - timedelta(minutes=1),
        expires_at=now + timedelta(minutes=5),
    )
    binding_digest = "4" * 64
    hold = Topic59MaintenanceHoldV1(
        hold_id=derive_hold_id(binding_digest, maintenance_scope_digest(scope)),
        package_binding_digest=binding_digest,
        scope=scope,
    )
    hold_bytes = canonical_document(hold)
    authority = Topic59MaintenanceAuthorityV1(
        package_binding_digest=hold.package_binding_digest,
        hold_sha256=hashlib.sha256(hold_bytes).hexdigest(),
        hold=hold,
    )
    maintenance = profile / "data" / "topic59-maintenance-r71b"
    maintenance.mkdir(parents=True, mode=0o700)
    _ = maintenance.chmod(0o700)
    _write_owned(maintenance / "hold.json", hold_bytes, 0o600)
    _write_owned(maintenance / "maintenance.lock", b"", 0o600)
    credentials.mkdir(mode=0o700)
    credential = credentials / "nutricoach-topic59-maintenance-r71b.json"
    _write_owned(credential, canonical_document(authority), 0o400)
    monkeypatch.setenv("CREDENTIALS_DIRECTORY", str(credentials))
    return MaintenanceFixture(
        profile=profile,
        credentials=credentials,
        maintenance=maintenance,
        credential=credential,
        hold=hold,
        authority=authority,
        hold_bytes=hold_bytes,
        projection=exact_projection,
        request=request,
        now=now,
    )


def write_bound_hold(fixture: MaintenanceFixture, hold: Topic59MaintenanceHoldV1) -> None:
    hold_bytes = canonical_document(hold)
    authority = Topic59MaintenanceAuthorityV1(
        package_binding_digest=hold.package_binding_digest,
        hold_sha256=hashlib.sha256(hold_bytes).hexdigest(),
        hold=hold,
    )
    _write_owned(fixture.maintenance / "hold.json", hold_bytes, 0o600)
    _write_owned(fixture.credential, canonical_document(authority), 0o400)


def _write_owned(path: Path, content: bytes, mode: int) -> None:
    if path.exists():
        _ = path.chmod(0o600)
    _ = path.write_bytes(content)
    _ = path.chmod(mode)


def change_group(path: Path) -> int:
    profile_group = path.parent.stat().st_gid
    alternate = next(group for group in os.getgroups() if group != profile_group)
    _ = os.chown(path, -1, alternate)
    return alternate


class RejectingLedger:
    claim_calls: int
    record_calls: int

    def __init__(self) -> None:
        self.claim_calls = 0
        self.record_calls = 0

    def claim(self, projection: Topic59Projection) -> Topic59LedgerAction:
        self.claim_calls += 1
        raise AssertionError(projection.logical_key)

    def record(
        self, action: Topic59LedgerAction, outcome: Topic59ProviderOutcome,
    ) -> Topic59LedgerState:
        self.record_calls += 1
        raise AssertionError((action.kind, outcome))


class RejectingTransport:
    send_calls: int
    edit_calls: int
    network_calls: int

    def __init__(self) -> None:
        self.send_calls = 0
        self.edit_calls = 0
        self.network_calls = 0

    async def send(
        self, *, chat_id: str, topic_id: str, text: str,
    ) -> Topic59ProviderOutcome:
        self.send_calls += 1
        self.network_calls += 1
        raise AssertionError((chat_id, topic_id, text))

    async def edit(
        self, *, chat_id: str, topic_id: str, message_id: str, text: str,
    ) -> Topic59ProviderOutcome:
        self.edit_calls += 1
        self.network_calls += 1
        raise AssertionError((chat_id, topic_id, message_id, text))


@dataclass(frozen=True, slots=True)
class DeniedObservation:
    result: Topic59PublicationResult
    claim_calls: int
    record_calls: int
    send_calls: int
    edit_calls: int
    network_calls: int


async def publish_with_tripwires(fixture: MaintenanceFixture) -> DeniedObservation:
    ledger = RejectingLedger()
    transport = RejectingTransport()
    projector = Topic59DayCardProjector(
        fixture.ledger,
        Topic59MaintenanceGate.for_profile(fixture.profile),
        ledger,
    )
    result = await projector.publish(fixture.request, transport)
    return DeniedObservation(
        result=result,
        claim_calls=ledger.claim_calls,
        record_calls=ledger.record_calls,
        send_calls=transport.send_calls,
        edit_calls=transport.edit_calls,
        network_calls=transport.network_calls,
    )
