"""Unit tests for the ImageHub task-pipeline domain (project-workflow §3/§4 transitions).

The pipeline service is pure (plain functions over ``StageInfo`` lists — no Postgres, no FastAPI),
so the whole state machine is exercised here with plain data. The thin HTTP wrappers in
``imagehub_routes`` are verified live; this file owns the transition rules.

Covers: stage ordering, the new-task start state, TP1 finalize (advance / -> Ground Truth),
TP2 accept + accept-with-corrections, TP3 reject (-> first stage), the guards (wrong-state finalize/
review, bad decision), and RS1 reference-standard (Ground-Truth-only).
"""
from __future__ import annotations

import unittest

from src.imagehub_task_pipeline import (
    StageInfo,
    TaskPipelineError,
    compute_finalize,
    compute_review,
    first_stage,
    initial_transition,
    order_stages,
    stage_after,
    state_for_stage,
    validate_set_reference,
)

# A canonical pipeline: Label(0) -> Review_1(1) -> Review_2(2). Deliberately unsorted in the list
# so order_stages / first_stage / stage_after are actually exercised, not given pre-sorted input.
LABEL = StageInfo(id="s-label", kind="label", seq=0)
REV1 = StageInfo(id="s-rev1", kind="review", seq=1)
REV2 = StageInfo(id="s-rev2", kind="review", seq=2)
PIPELINE = [REV2, LABEL, REV1]  # out of order on purpose


class StageOrderingTests(unittest.TestCase):
    def test_order_stages_sorts_by_seq(self):
        self.assertEqual([s.id for s in order_stages(PIPELINE)], ["s-label", "s-rev1", "s-rev2"])

    def test_first_stage_is_lowest_seq(self):
        self.assertEqual(first_stage(PIPELINE).id, "s-label")

    def test_first_stage_empty_raises_409(self):
        with self.assertRaises(TaskPipelineError) as ctx:
            first_stage([])
        self.assertEqual(ctx.exception.status, 409)

    def test_stage_after_returns_next(self):
        self.assertEqual(stage_after(PIPELINE, "s-label").id, "s-rev1")
        self.assertEqual(stage_after(PIPELINE, "s-rev1").id, "s-rev2")

    def test_stage_after_last_is_none(self):
        self.assertIsNone(stage_after(PIPELINE, "s-rev2"))

    def test_stage_after_unknown_or_none_is_none(self):
        self.assertIsNone(stage_after(PIPELINE, "nope"))
        self.assertIsNone(stage_after(PIPELINE, None))

    def test_state_for_stage(self):
        self.assertEqual(state_for_stage(LABEL), "inLabel")
        self.assertEqual(state_for_stage(REV1), "inReview")


class InitialTransitionTests(unittest.TestCase):
    def test_new_task_starts_inlabel_at_first_stage(self):
        t = initial_transition(PIPELINE)
        self.assertEqual(t.pipeline_state, "inLabel")
        self.assertEqual(t.current_stage_id, "s-label")
        self.assertEqual(t.queue_status, "assigned")

    def test_new_task_starts_inreview_when_first_stage_is_review(self):
        t = initial_transition([REV1])
        self.assertEqual(t.pipeline_state, "inReview")
        self.assertEqual(t.current_stage_id, "s-rev1")


class FinalizeTests(unittest.TestCase):
    def test_finalize_label_advances_to_first_review(self):
        t = compute_finalize("inLabel", "s-label", PIPELINE)
        self.assertEqual(t.pipeline_state, "inReview")
        self.assertEqual(t.current_stage_id, "s-rev1")
        self.assertEqual(t.queue_status, "assigned")

    def test_finalize_advances_label_to_next_label(self):
        # PreLabel(0,label) -> Label(1,label): finalizing the first stays inLabel at the next.
        prelabel = StageInfo(id="s-pre", kind="label", seq=0)
        label = StageInfo(id="s-lab", kind="label", seq=1)
        t = compute_finalize("inLabel", "s-pre", [prelabel, label])
        self.assertEqual(t.pipeline_state, "inLabel")
        self.assertEqual(t.current_stage_id, "s-lab")

    def test_finalize_single_label_goes_to_ground_truth(self):
        t = compute_finalize("inLabel", "s-label", [LABEL])
        self.assertEqual(t.pipeline_state, "groundTruth")
        self.assertIsNone(t.current_stage_id)

    def test_finalize_when_not_inlabel_raises_409(self):
        with self.assertRaises(TaskPipelineError) as ctx:
            compute_finalize("inReview", "s-rev1", PIPELINE)
        self.assertEqual(ctx.exception.status, 409)

    def test_finalize_when_groundtruth_raises_409(self):
        with self.assertRaises(TaskPipelineError):
            compute_finalize("groundTruth", None, PIPELINE)


class ReviewTests(unittest.TestCase):
    def test_accept_advances_to_next_review(self):
        t = compute_review("inReview", "s-rev1", PIPELINE, "accept")
        self.assertEqual(t.pipeline_state, "inReview")
        self.assertEqual(t.current_stage_id, "s-rev2")

    def test_accept_on_last_review_goes_to_ground_truth(self):
        t = compute_review("inReview", "s-rev2", PIPELINE, "accept")
        self.assertEqual(t.pipeline_state, "groundTruth")
        self.assertIsNone(t.current_stage_id)

    def test_accept_with_corrections_advances(self):
        t = compute_review("inReview", "s-rev1", PIPELINE, "acceptWithCorrections")
        self.assertEqual(t.pipeline_state, "inReview")
        self.assertEqual(t.current_stage_id, "s-rev2")

    def test_reject_returns_to_first_stage(self):
        t = compute_review("inReview", "s-rev2", PIPELINE, "reject")
        self.assertEqual(t.pipeline_state, "inLabel")
        self.assertEqual(t.current_stage_id, "s-label")
        self.assertEqual(t.queue_status, "assigned")

    def test_review_when_not_inreview_raises_409(self):
        with self.assertRaises(TaskPipelineError) as ctx:
            compute_review("inLabel", "s-label", PIPELINE, "accept")
        self.assertEqual(ctx.exception.status, 409)

    def test_review_invalid_decision_raises_422(self):
        with self.assertRaises(TaskPipelineError) as ctx:
            compute_review("inReview", "s-rev1", PIPELINE, "maybe")
        self.assertEqual(ctx.exception.status, 422)


class ReferenceStandardTests(unittest.TestCase):
    def test_set_reference_ok_on_ground_truth(self):
        # Should not raise.
        validate_set_reference("groundTruth", True)

    def test_set_reference_blocked_off_ground_truth(self):
        with self.assertRaises(TaskPipelineError) as ctx:
            validate_set_reference("inLabel", True)
        self.assertEqual(ctx.exception.status, 409)

    def test_unset_reference_allowed_in_any_state(self):
        # Removing the flag is always fine, even mid-pipeline.
        validate_set_reference("inLabel", False)
        validate_set_reference("inReview", False)


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