from __future__ import annotations

from typing import Any

import pytest

from icsd.core.invariants import Invariant, InvariantSet
from icsd.core.migration import (
    MigrationReport,
    QuarantineRecord,
    migrate_legacy_state,
    migrate_legacy_states,
)


def _legacy_invariants() -> InvariantSet:
    return InvariantSet(
        [
            Invariant(
                name="non_negative_balance",
                check=lambda state: isinstance(state.get("balance"), int) and state["balance"] >= 0,
            ),
            Invariant(
                name="owner_required",
                check=lambda state: (
                    isinstance(state.get("owner"), str) and bool(state["owner"].strip())
                ),
            ),
        ]
    )


def test_migrate_legacy_state_returns_validated_copy_for_valid_state() -> None:
    source = {"balance": 50, "owner": "ops"}

    migrated, quarantine = migrate_legacy_state(
        source,
        invariants=_legacy_invariants(),
    )

    assert quarantine is None
    assert migrated == source
    assert migrated is not source


def test_migrate_legacy_state_applies_transform_and_repairs_invalid_state() -> None:
    source = {"balance": -10, "owner": ""}

    def repair(state: dict[str, Any]) -> None:
        state["balance"] = 0
        state["owner"] = "migrated-owner"

    migrated, quarantine = migrate_legacy_state(
        source,
        invariants=_legacy_invariants(),
        transform=repair,
    )

    assert quarantine is None
    assert migrated == {"balance": 0, "owner": "migrated-owner"}
    assert source == {"balance": -10, "owner": ""}


def test_migrate_legacy_state_quarantines_unrepaired_state_with_failures() -> None:
    migrated, quarantine = migrate_legacy_state(
        {"balance": -5, "owner": ""},
        invariants=_legacy_invariants(),
    )

    assert migrated is None
    assert isinstance(quarantine, QuarantineRecord)
    assert quarantine.reason == "legacy_state_invariant_violation"
    assert len(quarantine.failures) == 2
    assert quarantine.index == 0


def test_migrate_legacy_state_quarantines_non_mapping_value() -> None:
    migrated, quarantine = migrate_legacy_state(
        "legacy-row",
        invariants=_legacy_invariants(),
        index=4,
    )

    assert migrated is None
    assert isinstance(quarantine, QuarantineRecord)
    assert quarantine.reason == "legacy_state_not_mapping"
    assert quarantine.index == 4
    assert quarantine.details["error"] == "legacy state must be a mapping."


def test_migrate_legacy_state_quarantines_invalid_transform_return_type() -> None:
    migrated, quarantine = migrate_legacy_state(
        {"balance": 10, "owner": "ops"},
        invariants=_legacy_invariants(),
        transform=lambda _: 123,
    )

    assert migrated is None
    assert isinstance(quarantine, QuarantineRecord)
    assert quarantine.reason == "legacy_state_transform_not_mapping"
    assert quarantine.details["returned_type"] == "int"


def test_migrate_legacy_state_uses_reason_resolver_when_quarantined() -> None:
    migrated, quarantine = migrate_legacy_state(
        {"balance": -1, "owner": "ops"},
        invariants=_legacy_invariants(),
        reason_resolver=lambda failures: f"custom/{failures[0].invariant}",
    )

    assert migrated is None
    assert isinstance(quarantine, QuarantineRecord)
    assert quarantine.reason == "custom/non_negative_balance"


def test_migrate_legacy_states_batches_mixed_results() -> None:
    states: list[Any] = [
        {"balance": 10, "owner": "ops"},
        {"balance": -3, "owner": "legacy"},
        42,
        {"balance": 0, "owner": ""},
    ]

    def repair_negative_only(state: dict[str, Any]) -> None:
        if isinstance(state.get("balance"), int) and state["balance"] < 0:
            state["balance"] = 0

    report = migrate_legacy_states(
        states,
        invariants=_legacy_invariants(),
        transform=repair_negative_only,
    )

    assert isinstance(report, MigrationReport)
    assert report.total_count == 4
    assert report.migrated_count == 2
    assert report.quarantined_count == 2
    assert [item.index for item in report.quarantined] == [2, 3]


def test_migrate_legacy_states_rejects_string_iterable_input() -> None:
    with pytest.raises(TypeError, match="states must be an iterable"):
        migrate_legacy_states("bad-input", invariants=_legacy_invariants())


def test_migrate_legacy_state_rejects_empty_reason_from_resolver() -> None:
    with pytest.raises(ValueError, match="quarantine reason must be a non-empty string"):
        migrate_legacy_state(
            {"balance": -1, "owner": "ops"},
            invariants=_legacy_invariants(),
            reason_resolver=lambda _: "",
        )


def test_quarantine_record_deep_copies_nested_details_on_input_and_output() -> None:
    details = {"meta": {"reason": "legacy"}, "steps": ["inspect"]}
    quarantine = QuarantineRecord(
        index=0,
        reason="legacy_state_invariant_violation",
        original_state={"balance": -1},
        details=details,
    )

    details["meta"]["reason"] = "mutated"
    details["steps"].append("tampered")

    payload = quarantine.as_dict()

    assert payload["details"] == {
        "meta": {"reason": "legacy"},
        "steps": ["inspect"],
    }

    payload["details"]["meta"]["reason"] = "changed"
    payload["details"]["steps"].append("output-mutation")

    assert quarantine.as_dict()["details"] == {
        "meta": {"reason": "legacy"},
        "steps": ["inspect"],
    }
