from __future__ import annotations

from collections.abc import Mapping
from typing import Any

import pytest

from icsd.core.batch import validate_states
from icsd.core.invariants import Invariant, InvariantSet
from icsd.tools.batch import (
    validate_states as tool_validate_states,
)
from icsd.tools.batch import (
    validate_states_report,
)


def _non_negative_invariants() -> InvariantSet:
    return InvariantSet(
        [
            Invariant(
                name="non_negative",
                check=lambda state: isinstance(state.get("value"), int) and state["value"] >= 0,
            )
        ]
    )


def test_validate_states_returns_all_pass_results_for_valid_batch() -> None:
    report = validate_states(
        _non_negative_invariants(),
        [
            {"value": 1},
            {"value": 2},
        ],
    )

    assert report.total_count == 2
    assert report.passed_count == 2
    assert report.failed_count == 0
    assert [result.index for result in report.results] == [0, 1]
    assert all(result.valid for result in report.results)


def test_validate_states_mixed_batch_preserves_order_and_failure_envelopes() -> None:
    report = validate_states(
        _non_negative_invariants(),
        [
            {"value": 1},
            {"value": -1},
            "not-a-mapping",
            {"value": 0},
        ],
    )

    assert [result.index for result in report.results] == [0, 1, 2, 3]
    assert [result.valid for result in report.results] == [True, False, False, True]
    assert report.total_count == 4
    assert report.passed_count == 2
    assert report.failed_count == 2
    assert report.results[1].failures[0].invariant == "non_negative"
    assert report.results[2].failures[0].code == "state_not_mapping"
    assert report.results[2].failures[0].details["state_type"] == "str"


def test_validate_states_records_non_mapping_item_without_raising() -> None:
    report = validate_states(_non_negative_invariants(), [42])

    assert report.total_count == 1
    assert report.failed_count == 1
    assert report.results[0].index == 0
    assert report.results[0].valid is False
    assert report.results[0].failures[0].invariant == "state_mapping_required"
    assert report.results[0].failures[0].code == "state_not_mapping"


def test_batch_report_as_dict_has_deterministic_shape_and_order() -> None:
    report = validate_states(
        _non_negative_invariants(),
        ["bad-state"],
        context={"request_id": "req-0075"},
    )

    payload = report.as_dict()

    assert list(payload) == [
        "context",
        "total_count",
        "passed_count",
        "failed_count",
        "results",
    ]
    assert list(payload["results"][0]) == ["index", "status", "failure_count", "failures"]
    assert list(payload["results"][0]["failures"][0]) == [
        "invariant",
        "code",
        "message",
        "details",
    ]


def test_validate_states_context_is_copied_and_isolation_is_preserved() -> None:
    context = {"request_id": "req-0075", "metadata": {"mode": "initial"}}

    report = validate_states(
        _non_negative_invariants(),
        [{"value": 1}],
        context=context,
    )
    context["metadata"]["mode"] = "mutated"

    assert report.context is not None
    assert report.context["metadata"]["mode"] == "initial"
    serialised = report.as_dict()
    serialised["context"]["metadata"]["mode"] = "changed"
    assert report.context["metadata"]["mode"] == "initial"


def test_validate_states_does_not_leak_mutations_to_input_states() -> None:
    def mutating_check(state: Mapping[str, Any]) -> bool:
        if isinstance(state, dict):
            state["touched"] = True
        return True

    invariants = InvariantSet([Invariant(name="mutating", check=mutating_check)])
    source_state = {"value": 1}

    report = validate_states(invariants, [source_state])

    assert report.passed_count == 1
    assert "touched" not in source_state


def test_validate_states_argument_guards_reject_invalid_input_types() -> None:
    with pytest.raises(TypeError, match="invariant_set must be an InvariantSet instance"):
        validate_states(object(), [])  # type: ignore[arg-type]

    with pytest.raises(TypeError, match="states must be an iterable of state values"):
        validate_states(_non_negative_invariants(), "bad-input")

    with pytest.raises(TypeError, match="context must be a mapping when provided"):
        validate_states(
            _non_negative_invariants(),
            [{"value": 1}],
            context="bad-context",  # type: ignore[arg-type]
        )


def test_tools_batch_bridge_matches_core_batch_contract() -> None:
    states = [{"value": 5}, {"value": -1}]
    context = {"request_id": "req-bridge"}

    core_report = validate_states(_non_negative_invariants(), states, context=context)
    tool_report = tool_validate_states(_non_negative_invariants(), states, context=context)
    tool_payload = validate_states_report(_non_negative_invariants(), states, context=context)

    assert tool_report.as_dict() == core_report.as_dict()
    assert tool_payload == core_report.as_dict()
