"""Synthetic counting invariants, not assistant, inference, or hardware tests."""
from copy import deepcopy
from itertools import permutations
import json
from pathlib import Path
import unittest

from request_accounting import summarize_attempts

PACK = Path(__file__).resolve().parent


class RequestAccountingTests(unittest.TestCase):
    def setUp(self):
        self.fixture = json.loads((PACK / "synthetic-request-attempts.json").read_text())
        self.records = self.fixture["attempts"]

    def test_fixture_is_labelled_as_synthetic_preparation(self):
        self.assertIs(self.fixture["synthetic"], True)
        self.assertEqual(self.fixture["status"], "synthetic_software_fixture_not_a_run")
        self.assertEqual(self.fixture["study_id"], "AM-RP-001")
        self.assertEqual(self.fixture["author"], "Anant Mishra")

    def test_frozen_fixture_matches_declared_counts(self):
        self.assertEqual(summarize_attempts(self.records), self.fixture["expected_summary"])

    def test_partitions_have_no_lost_attempts_or_logical_requests(self):
        for size in range(len(self.records) + 1):
            result = summarize_attempts(self.records[:size])
            a, logical = result["attempts"], result["logical_requests"]
            self.assertEqual(a["total"], sum(a[key] for key in ("success_response", "error_response", "unknown")))
            self.assertEqual(logical["total"], sum(logical[key] for key in ("with_success_response", "without_success_with_unknown", "only_error_responses")))

    def test_success_does_not_erase_an_unknown_sibling(self):
        result = summarize_attempts([r for r in self.records if r["logical_request_id"] == "SYN-R04"])
        self.assertEqual(result["logical_requests"]["with_success_response"], 1)
        self.assertEqual(result["logical_requests"]["with_any_unknown_attempt"], 1)
        self.assertEqual(result["attempts"]["unknown"], 1)

    def test_timeout_alone_is_not_a_received_success_or_error(self):
        result = summarize_attempts([self.records[3]])
        self.assertEqual(result["logical_requests"]["without_success_with_unknown"], 1)
        self.assertEqual(result["attempts"]["success_response"], 0)
        self.assertEqual(result["attempts"]["error_response"], 0)

    def test_multiple_success_responses_are_exposed(self):
        result = summarize_attempts(self.records[-2:])
        self.assertEqual(result["attempts"]["success_response"], 2)
        self.assertEqual(result["logical_requests"]["total"], 1)
        self.assertEqual(result["logical_requests"]["with_multiple_success_responses"], 1)

    def test_duplicate_log_record_is_rejected(self):
        with self.assertRaisesRegex(ValueError, "Duplicate attempt"):
            summarize_attempts(self.records + [deepcopy(self.records[0])])

    def test_conflicting_record_for_same_attempt_is_rejected(self):
        changed = {**self.records[0], "logical_request_id": "SYN-DIFFERENT"}
        with self.assertRaisesRegex(ValueError, "Duplicate attempt"):
            summarize_attempts(self.records + [changed])

    def test_invalid_outcome_and_observation_are_rejected(self):
        for key, value in (("outcome", "probably_worked"), ("observation", "guessed"), ("outcome", []), ("observation", None)):
            with self.subTest(key=key, value=value), self.assertRaises(ValueError):
                summarize_attempts([{**self.records[0], key: value}])

    def test_transport_observation_cannot_claim_response(self):
        for outcome, observation in (("success_response", "timeout"), ("error_response", "connection_lost"), ("unknown", "response")):
            with self.subTest(outcome=outcome), self.assertRaises(ValueError):
                summarize_attempts([{**self.records[0], "outcome": outcome, "observation": observation}])

    def test_bad_or_naive_timestamps_are_rejected(self):
        for value in ("not-a-date", "2026-01-16T09:00:00", None):
            with self.subTest(value=value), self.assertRaises(ValueError):
                summarize_attempts([{**self.records[0], "started_at": value}])

    def test_observation_before_start_is_rejected(self):
        with self.assertRaisesRegex(ValueError, "precede"):
            summarize_attempts([{**self.records[0], "observed_at": "2026-01-16T08:59:59Z"}])

    def test_equivalent_timezone_offsets_are_compared_as_instants(self):
        record = {**self.records[0], "started_at": "2026-01-16T13:00:00+04:00", "observed_at": "2026-01-16T09:00:01Z"}
        self.assertEqual(summarize_attempts([record])["attempts"]["total"], 1)

    def test_missing_extra_fields_and_empty_ids_are_rejected(self):
        for record in ({k: v for k, v in self.records[0].items() if k != "outcome"}, {**self.records[0], "inferred_success": True}, {**self.records[0], "attempt_id": " "}, {**self.records[0], "logical_request_id": None}):
            with self.subTest(record=record), self.assertRaises(ValueError):
                summarize_attempts([record])

    def test_order_independence_and_input_preservation(self):
        original = deepcopy(self.records)
        reference = summarize_attempts(self.records)
        for records in permutations(self.records[:3]):
            self.assertEqual(summarize_attempts(list(records) + self.records[3:]), reference)
        self.assertEqual(self.records, original)

    def test_empty_input_is_counts_not_a_success_rate(self):
        result = summarize_attempts([])
        self.assertTrue(all(value == 0 for group in result.values() for value in group.values()))
        with self.assertRaises(ValueError):
            summarize_attempts({})


if __name__ == "__main__":
    unittest.main()
