#!/usr/bin/env python3
"""
State-logic tests for the stop/return synthetic demo.

Run with:
    python3 -m unittest test_state_machine.py -v
or:
    python3 test_state_machine.py
"""
import unittest

from state_machine import SchedulingBlocked, ReversalNotAllowed, Task


class StopReturnStateLogicTests(unittest.TestCase):
    def test_stopped_local_producer_cannot_schedule_new_work(self):
        """A stopped local producer must refuse new work, with an explanation,
        not silently accept it."""
        task = Task(id="t1", description="update description")
        task.stop()
        self.assertEqual(task.local_state, "stopped")
        with self.assertRaises(SchedulingBlocked):
            task.schedule_new_work()

    def test_pending_external_effect_may_later_resolve_confirmed(self):
        """An in-flight external effect must be allowed to resolve to confirmed
        later; the model must not treat "stopped locally" as a terminal
        external verdict."""
        task = Task(id="t2", description="catalog sync write")
        task.stop()
        task.resolve_external("pending")
        self.assertEqual(task.external_state, "pending")
        task.resolve_external("confirmed", evidence_note="Receipt arrived after stop.")
        self.assertEqual(task.external_state, "confirmed")

    def test_unknown_cannot_become_failed_from_elapsed_time_alone(self):
        """unknown must never be auto-promoted to a failure state purely
        because time passed with no new evidence. The model exposes this as
        timeout_tick(), which must be a pure no-op on external_state."""
        task = Task(id="t3", description="catalog sync write")
        task.stop()
        task.resolve_external("unknown", evidence_note="No receipt before stop completed.")
        for _ in range(5):
            task.timeout_tick()
        self.assertEqual(task.external_state, "unknown")
        # There is no "failed" external state in this model at all, which is
        # itself part of the guarantee under test.
        from state_machine import EXTERNAL_STATES

        self.assertNotIn("failed", EXTERNAL_STATES)

    def test_changed_version_target_or_amount_invalidates_prior_approval(self):
        """A change to version, target, or amount after approval must
        invalidate that approval. Renewed approval is required before the
        task may proceed; the model never silently re-approves."""
        task = Task(id="t4", description="update description")
        task.grant_approval(version="v3", target="SKU-4471", amount=None)
        self.assertTrue(task.has_valid_approval())

        task.change_request(target="SKU-9999")
        self.assertFalse(task.has_valid_approval())

        # Also check version-only and amount-only changes independently.
        task_v = Task(id="t4v", description="x")
        task_v.grant_approval(version="v3", target="SKU-1", amount=10.0)
        task_v.change_request(version="v4")
        self.assertFalse(task_v.has_valid_approval())

        task_a = Task(id="t4a", description="x")
        task_a.grant_approval(version="v3", target="SKU-1", amount=10.0)
        task_a.change_request(amount=12.0)
        self.assertFalse(task_a.has_valid_approval())

        # An unchanged resubmission should not invalidate the approval.
        task_same = Task(id="t4s", description="x")
        task_same.grant_approval(version="v3", target="SKU-1", amount=10.0)
        task_same.change_request(version="v3", target="SKU-1", amount=10.0)
        self.assertTrue(task_same.has_valid_approval())

    def test_reversal_only_permitted_against_confirmed_reversible_effect(self):
        """Reversal must be rejected, with an explanation, against anything
        other than a confirmed effect that is itself marked reversible.
        It must never be silently blocked with no explanation, and it must
        never succeed against pending/unknown/non-reversible-confirmed."""
        # Pending: rejected.
        pending_task = Task(id="t5a", description="x")
        pending_task.stop()
        pending_task.resolve_external("pending")
        with self.assertRaises(ReversalNotAllowed) as ctx:
            pending_task.reverse()
        self.assertIn("not confirmed", str(ctx.exception))

        # Unknown: rejected.
        unknown_task = Task(id="t5b", description="x")
        unknown_task.stop()
        unknown_task.resolve_external("unknown")
        with self.assertRaises(ReversalNotAllowed):
            unknown_task.reverse()

        # Confirmed but not reversible: rejected, routed to compensation language.
        not_reversible_task = Task(id="t5c", description="x")
        not_reversible_task.stop()
        not_reversible_task.resolve_external("confirmed")
        not_reversible_task.reversible = False
        with self.assertRaises(ReversalNotAllowed) as ctx2:
            not_reversible_task.reverse()
        self.assertIn("compensation", str(ctx2.exception))

        # Confirmed and reversible: permitted.
        reversible_task = Task(id="t5d", description="x")
        reversible_task.stop()
        reversible_task.resolve_external("confirmed")
        reversible_task.reversible = True
        reversible_task.reverse()
        self.assertEqual(reversible_task.external_state, "canceled")


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