"""
tests/test_session.py — Unit tests for fw_server.session (SessionLedger).

Tests Channel 3 of the Hayat Bridge: session state tracking.

Coverage:
  - GRADE_ORDER / _grade_index
  - CallRecord creation
  - SessionLedger.record_call (happy path, grade ceiling, gate tracking)
  - SessionLedger.record_kavaid_failure
  - SessionLedger query properties (call_count, calls, get_grade_ceiling, etc.)
  - SessionLedger.get_cumulative_context
  - SessionLedger.get_cascade_status (all degradation levels)
  - SessionLedger.reset
  - Module-level singleton (get_ledger / reset_ledger)
  - KV₃ compliance: record_call never raises
"""

from __future__ import annotations

import unittest
from fw_server.session import (
    GRADE_ORDER,
    CallRecord,
    SessionLedger,
    _grade_index,
    get_ledger,
    reset_ledger,
)


class TestGradeOrder(unittest.TestCase):
    """GRADE_ORDER and _grade_index."""

    def test_order_is_ascending(self):
        self.assertEqual(GRADE_ORDER, ["Tasavvur", "Tasdik", "İlmelyakîn"])

    def test_grade_index_valid(self):
        self.assertEqual(_grade_index("Tasavvur"), 0)
        self.assertEqual(_grade_index("Tasdik"), 1)
        self.assertEqual(_grade_index("İlmelyakîn"), 2)

    def test_grade_index_unknown(self):
        self.assertEqual(_grade_index("Hakkalyakîn"), -1)
        self.assertEqual(_grade_index(""), -1)
        self.assertEqual(_grade_index("bogus"), -1)


class TestCallRecord(unittest.TestCase):
    """CallRecord dataclass."""

    def test_minimal(self):
        rec = CallRecord(tool_name="dusun")
        self.assertEqual(rec.tool_name, "dusun")
        self.assertIsNone(rec.grade)
        self.assertIsNone(rec.gate)
        self.assertIsNone(rec.composite_score)
        self.assertIsNone(rec.kavaid_pass_count)
        self.assertEqual(rec.anomalies, [])
        self.assertEqual(rec.extra, {})

    def test_full(self):
        rec = CallRecord(
            tool_name="validate_stage",
            grade="Tasdik",
            gate="WARN",
            composite_score=0.72,
            kavaid_pass_count=6,
            anomalies=["ZERO-SCORE: FOL"],
            extra={"stage": "3"},
        )
        self.assertEqual(rec.grade, "Tasdik")
        self.assertEqual(rec.gate, "WARN")
        self.assertAlmostEqual(rec.composite_score, 0.72)
        self.assertEqual(rec.kavaid_pass_count, 6)
        self.assertEqual(rec.anomalies, ["ZERO-SCORE: FOL"])
        self.assertEqual(rec.extra, {"stage": "3"})


class TestSessionLedgerRecording(unittest.TestCase):
    """SessionLedger recording API."""

    def setUp(self):
        self.ledger = SessionLedger()

    def test_empty_ledger(self):
        self.assertEqual(self.ledger.call_count, 0)
        self.assertEqual(self.ledger.calls, [])
        self.assertIsNone(self.ledger.get_grade_ceiling())
        self.assertIsNone(self.ledger.get_last_gate())
        self.assertFalse(self.ledger.has_failure())
        self.assertFalse(self.ledger.has_warning())
        self.assertEqual(self.ledger.kavaid_failures, [])

    def test_record_single_call(self):
        self.ledger.record_call("dusun", grade="Tasdik", composite_score=0.73)
        self.assertEqual(self.ledger.call_count, 1)
        self.assertEqual(self.ledger.calls[0].tool_name, "dusun")
        self.assertEqual(self.ledger.calls[0].grade, "Tasdik")

    def test_record_multiple_calls(self):
        self.ledger.record_call("dusun", grade="İlmelyakîn")
        self.ledger.record_call("validate_stage", grade="Tasdik", gate="WARN")
        self.ledger.record_call("check_kavaid")
        self.assertEqual(self.ledger.call_count, 3)

    def test_calls_returns_copy(self):
        """Returned list must be a copy, not a reference to internals."""
        self.ledger.record_call("dusun")
        calls = self.ledger.calls
        calls.append(CallRecord(tool_name="injected"))
        self.assertEqual(self.ledger.call_count, 1)  # not 2


class TestGradeCeiling(unittest.TestCase):
    """Grade ceiling logic — lowest grade wins (can only go DOWN)."""

    def setUp(self):
        self.ledger = SessionLedger()

    def test_single_grade_sets_ceiling(self):
        self.ledger.record_call("dusun", grade="İlmelyakîn")
        self.assertEqual(self.ledger.get_grade_ceiling(), "İlmelyakîn")

    def test_ceiling_goes_down(self):
        self.ledger.record_call("dusun", grade="İlmelyakîn")
        self.ledger.record_call("validate_stage", grade="Tasavvur")
        self.assertEqual(self.ledger.get_grade_ceiling(), "Tasavvur")

    def test_ceiling_never_goes_up(self):
        self.ledger.record_call("dusun", grade="Tasavvur")
        self.ledger.record_call("validate_stage", grade="İlmelyakîn")
        self.assertEqual(self.ledger.get_grade_ceiling(), "Tasavvur")

    def test_unknown_grade_ignored(self):
        self.ledger.record_call("dusun", grade="Hakkalyakîn")
        self.assertIsNone(self.ledger.get_grade_ceiling())

    def test_none_grade_ignored(self):
        self.ledger.record_call("check_kavaid", grade=None)
        self.assertIsNone(self.ledger.get_grade_ceiling())

    def test_mixed_valid_invalid_grades(self):
        self.ledger.record_call("dusun", grade="bogus")
        self.ledger.record_call("validate_stage", grade="Tasdik")
        self.assertEqual(self.ledger.get_grade_ceiling(), "Tasdik")


class TestGateTracking(unittest.TestCase):
    """Gate history and query methods."""

    def setUp(self):
        self.ledger = SessionLedger()

    def test_no_gates(self):
        self.assertIsNone(self.ledger.get_last_gate())
        self.assertFalse(self.ledger.has_failure())
        self.assertFalse(self.ledger.has_warning())

    def test_pass_gate(self):
        self.ledger.record_call("validate_stage", gate="PASS")
        self.assertEqual(self.ledger.get_last_gate(), "PASS")
        self.assertFalse(self.ledger.has_failure())
        self.assertFalse(self.ledger.has_warning())

    def test_warn_gate(self):
        self.ledger.record_call("validate_stage", gate="WARN")
        self.assertTrue(self.ledger.has_warning())
        self.assertFalse(self.ledger.has_failure())

    def test_fail_gate(self):
        self.ledger.record_call("validate_stage", gate="FAIL")
        self.assertTrue(self.ledger.has_failure())

    def test_last_gate_is_most_recent(self):
        self.ledger.record_call("validate_stage", gate="PASS")
        self.ledger.record_call("validate_stage", gate="WARN")
        self.ledger.record_call("validate_stage", gate="FAIL")
        self.assertEqual(self.ledger.get_last_gate(), "FAIL")


class TestKavaidFailures(unittest.TestCase):
    """Kavaid failure tracking."""

    def setUp(self):
        self.ledger = SessionLedger()

    def test_no_failures(self):
        self.assertEqual(self.ledger.kavaid_failures, [])

    def test_record_failure(self):
        self.ledger.record_kavaid_failure("KV4")
        self.assertEqual(self.ledger.kavaid_failures, ["KV4"])

    def test_dedup(self):
        self.ledger.record_kavaid_failure("KV4")
        self.ledger.record_kavaid_failure("KV4")
        self.assertEqual(self.ledger.kavaid_failures, ["KV4"])

    def test_multiple_failures(self):
        self.ledger.record_kavaid_failure("KV4")
        self.ledger.record_kavaid_failure("KV7")
        self.assertEqual(self.ledger.kavaid_failures, ["KV4", "KV7"])

    def test_returns_copy(self):
        self.ledger.record_kavaid_failure("KV1")
        kf = self.ledger.kavaid_failures
        kf.append("KV99")
        self.assertEqual(len(self.ledger.kavaid_failures), 1)


class TestCumulativeContext(unittest.TestCase):
    """get_cumulative_context output structure."""

    def setUp(self):
        self.ledger = SessionLedger()

    def test_empty_context(self):
        ctx = self.ledger.get_cumulative_context()
        self.assertEqual(ctx["session_call_count"], 0)
        self.assertIsNone(ctx["grade_ceiling"])
        self.assertIsNone(ctx["last_gate"])
        self.assertFalse(ctx["has_failure"])
        self.assertFalse(ctx["has_warning"])
        self.assertEqual(ctx["kavaid_failures"], [])
        self.assertEqual(ctx["gate_history"], [])
        self.assertEqual(ctx["tool_sequence"], [])

    def test_populated_context(self):
        self.ledger.record_call("dusun", grade="Tasdik")
        self.ledger.record_call("validate_stage", grade="Tasavvur", gate="WARN")
        self.ledger.record_kavaid_failure("KV4")
        ctx = self.ledger.get_cumulative_context()
        self.assertEqual(ctx["session_call_count"], 2)
        self.assertEqual(ctx["grade_ceiling"], "Tasavvur")
        self.assertEqual(ctx["last_gate"], "WARN")
        self.assertFalse(ctx["has_failure"])
        self.assertTrue(ctx["has_warning"])
        self.assertEqual(ctx["kavaid_failures"], ["KV4"])
        self.assertEqual(ctx["gate_history"], ["WARN"])
        self.assertEqual(ctx["tool_sequence"], ["dusun", "validate_stage"])


class TestCascadeStatus(unittest.TestCase):
    """get_cascade_status degradation levels."""

    def setUp(self):
        self.ledger = SessionLedger()

    def test_nominal_no_calls(self):
        status = self.ledger.get_cascade_status()
        self.assertEqual(status["degradation_level"], "NOMINAL")

    def test_nominal_all_pass(self):
        self.ledger.record_call("validate_stage", gate="PASS")
        self.ledger.record_call("validate_stage", gate="PASS")
        status = self.ledger.get_cascade_status()
        self.assertEqual(status["degradation_level"], "NOMINAL")

    def test_degraded_single_warn(self):
        self.ledger.record_call("validate_stage", gate="WARN")
        status = self.ledger.get_cascade_status()
        self.assertEqual(status["degradation_level"], "DEGRADED")

    def test_degraded_multiple_warns(self):
        self.ledger.record_call("validate_stage", gate="WARN")
        self.ledger.record_call("validate_stage", gate="WARN")
        status = self.ledger.get_cascade_status()
        self.assertEqual(status["degradation_level"], "DEGRADED")
        self.assertIn("multiple", status["reason"])

    def test_critical_on_fail(self):
        self.ledger.record_call("validate_stage", gate="FAIL")
        status = self.ledger.get_cascade_status()
        self.assertEqual(status["degradation_level"], "CRITICAL")

    def test_critical_trumps_warn(self):
        self.ledger.record_call("validate_stage", gate="WARN")
        self.ledger.record_call("validate_stage", gate="FAIL")
        status = self.ledger.get_cascade_status()
        self.assertEqual(status["degradation_level"], "CRITICAL")

    def test_degraded_on_kavaid_failure(self):
        self.ledger.record_call("check_kavaid")  # no gate
        self.ledger.record_kavaid_failure("KV4")
        status = self.ledger.get_cascade_status()
        self.assertEqual(status["degradation_level"], "DEGRADED")
        self.assertIn("KV4", status["reason"])

    def test_nominal_call_without_gate(self):
        """A dusun call with no gate → still NOMINAL if no failures."""
        self.ledger.record_call("dusun", grade="Tasdik")
        status = self.ledger.get_cascade_status()
        self.assertEqual(status["degradation_level"], "NOMINAL")


class TestReset(unittest.TestCase):
    """SessionLedger.reset clears everything."""

    def test_reset_clears_all(self):
        ledger = SessionLedger()
        ledger.record_call("dusun", grade="Tasdik", gate="PASS")
        ledger.record_kavaid_failure("KV1")
        ledger.reset()
        self.assertEqual(ledger.call_count, 0)
        self.assertIsNone(ledger.get_grade_ceiling())
        self.assertIsNone(ledger.get_last_gate())
        self.assertEqual(ledger.kavaid_failures, [])
        self.assertFalse(ledger.has_failure())
        self.assertFalse(ledger.has_warning())


class TestSingleton(unittest.TestCase):
    """get_ledger / reset_ledger module-level singleton."""

    def setUp(self):
        reset_ledger()

    def tearDown(self):
        reset_ledger()

    def test_get_ledger_returns_same_instance(self):
        a = get_ledger()
        b = get_ledger()
        self.assertIs(a, b)

    def test_reset_ledger_creates_new_instance(self):
        a = get_ledger()
        a.record_call("dusun", grade="Tasdik")
        reset_ledger()
        b = get_ledger()
        self.assertIsNot(a, b)
        self.assertEqual(b.call_count, 0)

    def test_reset_ledger_when_none(self):
        """reset_ledger is safe to call before any get_ledger."""
        reset_ledger()  # should not raise


class TestKV3Compliance(unittest.TestCase):
    """KV₃: record_call MUST never raise, even with bad input."""

    def setUp(self):
        self.ledger = SessionLedger()

    def test_record_call_with_none_tool_name(self):
        """Even pathological input must not crash."""
        # tool_name is required positional, but we test that the ledger
        # wraps everything in try/except.
        try:
            self.ledger.record_call(None)  # type: ignore
        except Exception:
            self.fail("record_call raised on None tool_name")

    def test_record_kavaid_failure_with_none(self):
        try:
            self.ledger.record_kavaid_failure(None)  # type: ignore
        except Exception:
            self.fail("record_kavaid_failure raised on None")


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