"""Tests for the TestEntity pure domain object.

These tests exercise the state machine, lifecycle commands, domain events,
business rule enforcement, and the from_orm/apply_to round-trip — all
without any database or framework dependency.
"""

import uuid
from datetime import datetime, timedelta
from unittest.mock import MagicMock

import pytest

import sys, os

backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if backend_dir not in sys.path:
    sys.path.insert(0, backend_dir)

from app.domain.test_entity import (
    TestEntity,
    TestState,
    VALID_TRANSITIONS,
    DomainEvent,
)
from app.domain.errors import (
    BusinessRuleViolation,
    InvalidOperationError,
    InvalidStateTransition,
)


# ── Helpers ──────────────────────────────────────────────────────────


def _entity(state: str = "draft", **overrides) -> TestEntity:
    defaults = dict(
        id=uuid.uuid4(),
        state=TestState(state),
        red_validation_status=None,
        red_validated_by=None,
        red_validated_at=None,
        red_validation_notes=None,
        blue_validation_status=None,
        blue_validated_by=None,
        blue_validated_at=None,
        blue_validation_notes=None,
        execution_date=None,
        red_started_at=None,
        blue_started_at=None,
        paused_at=None,
        red_paused_seconds=0,
        blue_paused_seconds=0,
    )
    defaults.update(overrides)
    return TestEntity(**defaults)


def _fake_orm(state: str = "draft", **overrides) -> MagicMock:
    """Build a mock that looks like a SQLAlchemy Test model."""
    m = MagicMock()
    m.id = uuid.uuid4()
    m.state = state
    m.red_validation_status = None
    m.red_validated_by = None
    m.red_validated_at = None
    m.red_validation_notes = None
    m.blue_validation_status = None
    m.blue_validated_by = None
    m.blue_validated_at = None
    m.blue_validation_notes = None
    m.execution_date = None
    m.red_started_at = None
    m.blue_started_at = None
    m.paused_at = None
    m.red_paused_seconds = 0
    m.blue_paused_seconds = 0
    for k, v in overrides.items():
        setattr(m, k, v)
    return m


# ── 1. VALID_TRANSITIONS completeness ───────────────────────────────


def test_every_state_has_a_transition_entry():
    for s in TestState:
        assert s in VALID_TRANSITIONS, f"Missing entry for {s}"


def test_validated_is_terminal():
    assert VALID_TRANSITIONS[TestState.validated] == []


# ── 2. can_transition ────────────────────────────────────────────────


@pytest.mark.parametrize(
    "current, target, expected",
    [
        ("draft", "red_executing", True),
        ("draft", "validated", False),
        ("draft", "blue_evaluating", False),
        ("red_executing", "red_review", True),
        ("red_executing", "blue_evaluating", False),
        ("red_executing", "draft", False),
        ("red_review", "blue_evaluating", True),
        ("red_review", "red_executing", True),
        ("blue_evaluating", "blue_review", True),
        ("blue_evaluating", "in_review", False),
        ("blue_review", "in_review", True),
        ("blue_review", "blue_evaluating", True),
        ("in_review", "validated", True),
        ("in_review", "rejected", True),
        ("in_review", "draft", False),
        ("rejected", "draft", True),
        ("validated", "draft", False),
        ("validated", "rejected", False),
    ],
)
def test_can_transition(current, target, expected):
    e = _entity(current)
    assert e.can_transition(TestState(target)) is expected


# ── 3. transition_to (public API) ───────────────────────────────────


def test_transition_to_valid():
    e = _entity("draft")
    prev = e.transition_to(TestState.red_executing)
    assert prev == "draft"
    assert e.state == TestState.red_executing


def test_transition_to_accepts_string():
    e = _entity("draft")
    prev = e.transition_to("red_executing")
    assert prev == "draft"
    assert e.state == TestState.red_executing


def test_transition_to_accepts_foreign_enum():
    """Simulates models.enums.TestState (different class, same .value)."""
    import enum

    class ForeignState(str, enum.Enum):
        red_executing = "red_executing"

    e = _entity("draft")
    prev = e.transition_to(ForeignState.red_executing)
    assert prev == "draft"
    assert e.state == TestState.red_executing


def test_transition_to_invalid_raises():
    e = _entity("draft")
    with pytest.raises(InvalidStateTransition) as exc_info:
        e.transition_to("validated")
    assert exc_info.value.current_state == "draft"
    assert exc_info.value.target_state == "validated"
    assert "red_executing" in exc_info.value.valid_transitions


def test_transition_emits_state_changed_event():
    e = _entity("draft")
    e.transition_to("red_executing")
    evts = [ev for ev in e.events if ev.name == "state_changed"]
    assert len(evts) == 1
    assert evts[0].payload["previous"] == "draft"
    assert evts[0].payload["new"] == "red_executing"


# ── 4. Lifecycle: start_execution ────────────────────────────────────


def test_start_execution():
    e = _entity("draft")
    before = datetime.utcnow()
    e.start_execution()
    assert e.state == TestState.red_executing
    assert e.execution_date is not None
    assert e.red_started_at is not None
    assert e.execution_date >= before
    assert any(ev.name == "execution_started" for ev in e.events)


def test_start_execution_from_wrong_state():
    e = _entity("in_review")
    with pytest.raises(InvalidStateTransition):
        e.start_execution()


# ── 5. Lifecycle: submit_red_evidence ────────────────────────────────


def test_submit_red_evidence():
    e = _entity("red_executing", red_started_at=datetime.utcnow())
    total_paused = e.submit_red_evidence()
    assert e.state == TestState.red_review
    assert total_paused == 0
    assert e.blue_started_at is None


def test_submit_red_evidence_auto_resumes():
    paused_time = datetime.utcnow() - timedelta(seconds=30)
    e = _entity("red_executing", paused_at=paused_time, red_paused_seconds=10)
    total_paused = e.submit_red_evidence()
    assert e.paused_at is None
    assert total_paused >= 40


# ── 5b. Lifecycle: red_review gate ───────────────────────────────────


def test_submit_red_evidence_now_goes_to_red_review():
    e = _entity("red_executing", red_started_at=datetime.utcnow())
    total_paused = e.submit_red_evidence()
    assert e.state == TestState.red_review
    assert total_paused == 0
    # blue timer must NOT start yet — that's approve_red_review's job
    assert e.blue_started_at is None


def test_approve_red_review_moves_to_blue_evaluating():
    e = _entity("red_review")
    e.approve_red_review()
    assert e.state == TestState.blue_evaluating
    assert e.blue_started_at is not None
    assert e.blue_paused_seconds == 0
    assert any(ev.name == "red_review_approved" for ev in e.events)


def test_approve_red_review_wrong_state():
    e = _entity("red_executing")
    with pytest.raises(InvalidStateTransition):
        e.approve_red_review()


def test_reopen_red_review_moves_to_red_executing():
    e = _entity("red_review", red_paused_seconds=50)
    e.reopen_red_review()
    assert e.state == TestState.red_executing
    assert e.red_started_at is not None
    assert e.red_paused_seconds == 0
    assert any(ev.name == "red_review_reopened" for ev in e.events)


def test_reopen_red_review_wrong_state():
    e = _entity("blue_evaluating")
    with pytest.raises(InvalidStateTransition):
        e.reopen_red_review()


# ── 6. Lifecycle: submit_blue_evidence ───────────────────────────────


def test_submit_blue_evidence():
    e = _entity("blue_evaluating", blue_started_at=datetime.utcnow())
    total_paused = e.submit_blue_evidence()
    assert e.state == TestState.blue_review
    assert total_paused == 0


def test_submit_blue_evidence_auto_resumes():
    paused_time = datetime.utcnow() - timedelta(seconds=20)
    e = _entity("blue_evaluating", paused_at=paused_time, blue_paused_seconds=5)
    total_paused = e.submit_blue_evidence()
    assert e.paused_at is None
    assert total_paused >= 25


# ── 6b. Lifecycle: blue_review gate ──────────────────────────────────


def test_approve_blue_review_moves_to_in_review():
    e = _entity("blue_review")
    e.approve_blue_review()
    assert e.state == TestState.in_review
    assert any(ev.name == "blue_review_approved" for ev in e.events)


def test_approve_blue_review_wrong_state():
    e = _entity("blue_evaluating")
    with pytest.raises(InvalidStateTransition):
        e.approve_blue_review()


def test_reopen_blue_review_moves_to_blue_evaluating():
    e = _entity("blue_review", blue_paused_seconds=30)
    e.reopen_blue_review()
    assert e.state == TestState.blue_evaluating
    assert e.blue_started_at is not None
    assert e.blue_paused_seconds == 0
    assert any(ev.name == "blue_review_reopened" for ev in e.events)


def test_reopen_blue_review_wrong_state():
    e = _entity("in_review")
    with pytest.raises(InvalidStateTransition):
        e.reopen_blue_review()


def test_flag_blue_review_gap_moves_to_in_review():
    e = _entity("blue_review")
    e.flag_blue_review_gap()
    assert e.state == TestState.in_review
    assert any(ev.name == "blue_review_gap_flagged" for ev in e.events)


def test_flag_blue_review_gap_wrong_state():
    e = _entity("blue_evaluating")
    with pytest.raises(InvalidStateTransition):
        e.flag_blue_review_gap()


def test_validate_red_not_reachable_from_red_review():
    e = _entity("red_review")
    with pytest.raises(InvalidOperationError, match="must be in_review"):
        e.validate_red("approved", by=uuid.uuid4())


def test_validate_blue_not_reachable_from_blue_review():
    e = _entity("blue_review")
    with pytest.raises(InvalidOperationError, match="must be in_review"):
        e.validate_blue("approved", by=uuid.uuid4())


# ── 7. pause_timer / resume_timer ────────────────────────────────────


def test_pause_timer_in_red_executing():
    e = _entity("red_executing")
    e.pause_timer()
    assert e.paused_at is not None
    assert any(ev.name == "timer_paused" for ev in e.events)


def test_pause_timer_in_blue_evaluating():
    e = _entity("blue_evaluating")
    e.pause_timer()
    assert e.paused_at is not None


def test_pause_timer_wrong_state():
    e = _entity("draft")
    with pytest.raises(BusinessRuleViolation, match="Cannot pause"):
        e.pause_timer()


def test_pause_timer_already_paused():
    e = _entity("red_executing", paused_at=datetime.utcnow())
    with pytest.raises(BusinessRuleViolation, match="already paused"):
        e.pause_timer()


def test_resume_timer_red():
    paused_time = datetime.utcnow() - timedelta(seconds=10)
    e = _entity("red_executing", paused_at=paused_time, red_paused_seconds=5)
    secs = e.resume_timer()
    assert secs >= 10
    assert e.paused_at is None
    assert e.red_paused_seconds >= 15


def test_resume_timer_blue():
    paused_time = datetime.utcnow() - timedelta(seconds=5)
    e = _entity("blue_evaluating", paused_at=paused_time, blue_paused_seconds=0)
    secs = e.resume_timer()
    assert secs >= 5
    assert e.blue_paused_seconds >= 5


def test_resume_timer_not_paused():
    e = _entity("red_executing")
    with pytest.raises(BusinessRuleViolation, match="not paused"):
        e.resume_timer()


# ── 8. Dual validation ──────────────────────────────────────────────


def test_dual_validation_both_approved():
    e = _entity("in_review")
    user_r = uuid.uuid4()
    user_b = uuid.uuid4()

    e.validate_red("approved", by=user_r, notes="LGTM")
    assert e.state == TestState.in_review

    e.validate_blue("approved", by=user_b, notes="Detection OK")
    assert e.state == TestState.validated
    assert any(ev.name == "dual_validation_approved" for ev in e.events)


def test_dual_validation_red_rejects():
    e = _entity("in_review")
    e.validate_red("rejected", by=uuid.uuid4())
    assert e.state == TestState.rejected
    assert any(ev.name == "dual_validation_rejected" for ev in e.events)


def test_dual_validation_blue_rejects():
    """Red already approved; Blue then rejects — a genuine conflict, not a veto."""
    e = _entity("in_review")
    e.validate_red("approved", by=uuid.uuid4())
    e.validate_blue("rejected", by=uuid.uuid4())
    assert e.state == TestState.disputed
    assert any(ev.name == "dual_validation_disputed" for ev in e.events)


def test_dual_validation_blue_approved_then_red_rejects():
    """Blue already approved; Red then rejects — also a genuine conflict."""
    e = _entity("in_review")
    e.validate_blue("approved", by=uuid.uuid4())
    e.validate_red("rejected", by=uuid.uuid4())
    assert e.state == TestState.disputed


def test_dual_validation_both_rejected_from_disputed():
    """A disputed test where the approving lead flips to reject (via the
    plain validate call, not resolve_dispute_reject) still lands on the
    generic terminal 'rejected' state — both leads now agree it's bad."""
    e = _entity(
        "disputed",
        red_validation_status="approved",
        blue_validation_status="rejected",
    )
    e.validate_red("rejected", by=uuid.uuid4())
    assert e.state == TestState.rejected


def test_validate_wrong_state():
    e = _entity("draft")
    with pytest.raises(BusinessRuleViolation, match="must be in_review"):
        e.validate_red("approved", by=uuid.uuid4())


def test_validate_invalid_status():
    e = _entity("in_review")
    with pytest.raises(BusinessRuleViolation, match="approved.*rejected"):
        e.validate_red("maybe", by=uuid.uuid4())


def test_validate_red_sets_fields():
    e = _entity("in_review")
    uid = uuid.uuid4()
    e.validate_red("approved", by=uid, notes="ok")
    assert e.red_validation_status == "approved"
    assert e.red_validated_by == uid
    assert e.red_validated_at is not None
    assert e.red_validation_notes == "ok"


# ── 9. reopen ────────────────────────────────────────────────────────


def test_reopen_clears_all_fields():
    e = _entity(
        "rejected",
        red_validation_status="rejected",
        red_validated_by=uuid.uuid4(),
        red_validated_at=datetime.utcnow(),
        red_validation_notes="bad",
        blue_validation_status="approved",
        blue_validated_by=uuid.uuid4(),
        blue_validated_at=datetime.utcnow(),
        blue_validation_notes="ok",
        red_started_at=datetime.utcnow(),
        blue_started_at=datetime.utcnow(),
        paused_at=datetime.utcnow(),
        red_paused_seconds=100,
        blue_paused_seconds=200,
    )
    e.reopen()
    assert e.state == TestState.draft
    assert e.red_validation_status is None
    assert e.red_validated_by is None
    assert e.red_validated_at is None
    assert e.blue_validation_status is None
    assert e.blue_validated_by is None
    assert e.blue_validated_at is None
    assert e.red_started_at is None
    assert e.blue_started_at is None
    assert e.paused_at is None
    assert e.red_paused_seconds == 0
    assert e.blue_paused_seconds == 0
    assert any(ev.name == "test_reopened" for ev in e.events)


def test_reopen_from_non_rejected_fails():
    e = _entity("draft")
    with pytest.raises(InvalidStateTransition):
        e.reopen()


# ── 10. from_orm / apply_to round-trip ───────────────────────────────


def test_from_orm_apply_to_roundtrip():
    model = _fake_orm("draft")
    entity = TestEntity.from_orm(model)
    assert entity.state == TestState.draft
    assert entity.id == model.id

    entity.start_execution()
    entity.apply_to(model)

    assert model.state == TestState.red_executing
    assert model.execution_date is not None
    assert model.red_started_at is not None


def test_from_orm_coerces_string_state():
    model = _fake_orm("blue_evaluating")
    entity = TestEntity.from_orm(model)
    assert entity.state == TestState.blue_evaluating


def test_from_orm_handles_none_paused_seconds():
    model = _fake_orm("draft")
    model.red_paused_seconds = None
    model.blue_paused_seconds = None
    entity = TestEntity.from_orm(model)
    assert entity.red_paused_seconds == 0
    assert entity.blue_paused_seconds == 0


# ── 11. Full lifecycle (happy path) ─────────────────────────────────


def test_full_lifecycle_happy_path():
    e = _entity("draft")
    uid_red = uuid.uuid4()
    uid_blue = uuid.uuid4()

    e.start_execution()
    assert e.state == TestState.red_executing

    e.submit_red_evidence()
    assert e.state == TestState.red_review

    e.approve_red_review()
    assert e.state == TestState.blue_evaluating

    e.submit_blue_evidence()
    assert e.state == TestState.blue_review

    e.approve_blue_review()
    assert e.state == TestState.in_review

    e.validate_red("approved", by=uid_red)
    e.validate_blue("approved", by=uid_blue)
    assert e.state == TestState.validated
    assert e.is_terminal is True

    event_names = [ev.name for ev in e.events]
    assert "state_changed" in event_names
    assert "execution_started" in event_names
    assert "dual_validation_approved" in event_names


def test_full_lifecycle_rejection_reopen():
    e = _entity("draft")
    e.start_execution()
    e.submit_red_evidence()
    e.approve_red_review()
    e.submit_blue_evidence()
    e.approve_blue_review()
    e.validate_red("rejected", by=uuid.uuid4())
    assert e.state == TestState.rejected

    e.reopen()
    assert e.state == TestState.draft

    e.start_execution()
    assert e.state == TestState.red_executing


# ── 12. is_terminal property ────────────────────────────────────────


def test_is_terminal():
    assert _entity("validated").is_terminal is True
    assert _entity("rejected").is_terminal is False
    assert _entity("draft").is_terminal is False


# ── 13. resolve_dispute_reject ──────────────────────────────────────


def test_resolve_dispute_reject_to_red():
    e = _entity(
        "disputed",
        red_validation_status="approved",
        red_validated_by=uuid.uuid4(),
        red_validated_at=datetime.utcnow(),
        blue_validation_status="rejected",
        blue_validated_by=uuid.uuid4(),
        blue_validated_at=datetime.utcnow(),
        red_paused_seconds=50,
    )
    e.resolve_dispute_reject("red")
    assert e.state == TestState.red_executing
    assert e.red_validation_status is None
    assert e.red_validated_by is None
    assert e.blue_validation_status is None
    assert e.blue_validated_by is None
    assert e.red_started_at is not None
    assert e.red_paused_seconds == 0
    assert any(ev.name == "dispute_resolved_to_rework" and ev.payload["target"] == "red" for ev in e.events)


def test_resolve_dispute_reject_to_blue():
    e = _entity(
        "disputed",
        blue_validation_status="approved",
        blue_validated_by=uuid.uuid4(),
        red_validation_status="rejected",
        red_validated_by=uuid.uuid4(),
        blue_paused_seconds=20,
    )
    e.resolve_dispute_reject("blue")
    assert e.state == TestState.blue_evaluating
    assert e.red_validation_status is None
    assert e.blue_validation_status is None
    assert e.blue_started_at is not None
    assert e.blue_paused_seconds == 0
    assert any(ev.name == "dispute_resolved_to_rework" and ev.payload["target"] == "blue" for ev in e.events)


def test_resolve_dispute_reject_wrong_state():
    e = _entity("in_review")
    with pytest.raises(InvalidStateTransition):
        e.resolve_dispute_reject("red")
