"""
tests/test_fw_doctor.py — Tests for the fw_doctor diagnostic module.

Test categories:
  1. Types & Data        — CheckStatus, CheckResult construction
  2. Individual checks   — each of the 10 check functions
  3. Runner              — run_all orchestration, exit codes
  4. Module integrity    — imports, __init__ exports, __main__ entry
  5. Integration         — full doctor pipeline end-to-end

Design notes:
  Each check is itself tested for PASS (or WARN at worst) — a FAIL
  means the underlying module has a defect, not the doctor.
  The _timed decorator is tested via elapsed_ms > 0.
"""

import io
import unittest
from unittest.mock import patch

from fw_doctor.checks import (
    ALL_CHECKS,
    CheckResult,
    CheckStatus,
    check_imports,
    check_version,
    check_lenses,
    check_bileshke,
    check_kavaid,
    check_kaskad,
    check_hcp,
    check_framework_summaries,
    check_persistence,
    check_memory_bank,
)
from fw_doctor.runner import run_all, exit_code


# ================================================================
# 1. Types & Data
# ================================================================

class TestCheckTypes(unittest.TestCase):
    """CheckStatus and CheckResult fundamentals."""

    def test_check_status_values(self):
        self.assertEqual(CheckStatus.PASS.value, "pass")
        self.assertEqual(CheckStatus.WARN.value, "warn")
        self.assertEqual(CheckStatus.FAIL.value, "fail")
        self.assertEqual(CheckStatus.SKIP.value, "skip")

    def test_check_result_construction(self):
        r = CheckResult(
            name="test",
            status=CheckStatus.PASS,
            message="OK",
            elapsed_ms=1.23,
            detail={"key": "value"},
        )
        self.assertEqual(r.name, "test")
        self.assertEqual(r.status, CheckStatus.PASS)
        self.assertEqual(r.message, "OK")
        self.assertAlmostEqual(r.elapsed_ms, 1.23)
        self.assertEqual(r.detail, {"key": "value"})

    def test_check_result_defaults(self):
        r = CheckResult(name="t", status=CheckStatus.FAIL, message="nope")
        self.assertEqual(r.elapsed_ms, 0.0)
        self.assertIsNone(r.detail)


# ================================================================
# 2. Individual checks — each must produce a valid CheckResult
# ================================================================

class TestCheckImports(unittest.TestCase):
    def test_imports_pass(self):
        r = check_imports()
        self.assertIsInstance(r, CheckResult)
        self.assertEqual(r.name, "imports")
        self.assertEqual(r.status, CheckStatus.PASS)
        self.assertGreater(r.elapsed_ms, 0)

    def test_imports_detail_has_count(self):
        r = check_imports()
        self.assertIn("count", r.detail)
        self.assertGreaterEqual(r.detail["count"], 10)


class TestCheckVersion(unittest.TestCase):
    def test_version_not_fail(self):
        r = check_version()
        self.assertIn(r.status, (CheckStatus.PASS, CheckStatus.WARN))
        self.assertGreater(r.elapsed_ms, 0)


class TestCheckLenses(unittest.TestCase):
    def test_lenses_pass(self):
        r = check_lenses()
        self.assertIsInstance(r, CheckResult)
        self.assertEqual(r.name, "lenses")
        self.assertEqual(r.status, CheckStatus.PASS, f"Lenses failed: {r.message}")
        self.assertIn("results", r.detail)
        # All 7 should be present
        self.assertEqual(len(r.detail["results"]), 7)

    def test_lens_scores_bounded(self):
        """T6/KV₄: all scores must be < 1.0."""
        r = check_lenses()
        if r.status == CheckStatus.PASS:
            for lens_id, info in r.detail["results"].items():
                self.assertLess(info["score"], 1.0, f"{lens_id} score violates T6")


class TestCheckBileshke(unittest.TestCase):
    def test_bileshke_runs(self):
        r = check_bileshke()
        self.assertIsInstance(r, CheckResult)
        self.assertEqual(r.name, "bileshke")
        self.assertIn(r.status, (CheckStatus.PASS, CheckStatus.WARN))

    def test_bileshke_composite_bounded(self):
        """KV₄: composite must be in (0, 1)."""
        r = check_bileshke()
        if r.detail and "composite_score" in r.detail:
            self.assertLess(r.detail["composite_score"], 1.0)
            self.assertGreaterEqual(r.detail["composite_score"], 0.0)


class TestCheckKavaid(unittest.TestCase):
    def test_kavaid_runs(self):
        r = check_kavaid()
        self.assertIsInstance(r, CheckResult)
        self.assertEqual(r.name, "kavaid")
        self.assertIn(r.status, (CheckStatus.PASS, CheckStatus.WARN))

    def test_kavaid_has_results(self):
        r = check_kavaid()
        if r.detail and "results" in r.detail:
            self.assertIsInstance(r.detail["results"], dict)


class TestCheckKaskad(unittest.TestCase):
    def test_kaskad_pass(self):
        r = check_kaskad()
        self.assertIsInstance(r, CheckResult)
        self.assertEqual(r.name, "kaskad")
        self.assertEqual(r.status, CheckStatus.PASS, f"Kaskad failed: {r.message}")

    def test_kaskad_structure(self):
        r = check_kaskad()
        if r.detail:
            self.assertGreater(r.detail["nodes"], 0)
            self.assertGreater(r.detail["edges"], 0)
            self.assertGreater(r.detail["chains"], 0)


class TestCheckHCP(unittest.TestCase):
    def test_hcp_roundtrip(self):
        r = check_hcp()
        self.assertIsInstance(r, CheckResult)
        self.assertEqual(r.name, "hcp")
        self.assertEqual(r.status, CheckStatus.PASS, f"HCP failed: {r.message}")

    def test_hcp_detail(self):
        r = check_hcp()
        if r.detail:
            self.assertGreater(r.detail["n_chunks"], 0)
            self.assertGreater(r.detail["n_results"], 0)
            self.assertGreater(r.detail["top_score"], 0)


class TestCheckSummaries(unittest.TestCase):
    def test_summaries_pass(self):
        r = check_framework_summaries()
        self.assertIsInstance(r, CheckResult)
        self.assertEqual(r.name, "summaries")
        self.assertEqual(r.status, CheckStatus.PASS, f"Summaries failed: {r.message}")


class TestCheckPersistence(unittest.TestCase):
    def test_persistence_roundtrip(self):
        r = check_persistence()
        self.assertIsInstance(r, CheckResult)
        self.assertEqual(r.name, "persistence")
        self.assertEqual(r.status, CheckStatus.PASS, f"Persistence failed: {r.message}")


class TestCheckMemoryBank(unittest.TestCase):
    def test_memory_bank_export(self):
        r = check_memory_bank()
        self.assertIsInstance(r, CheckResult)
        self.assertEqual(r.name, "memory_bank")
        self.assertEqual(r.status, CheckStatus.PASS, f"Memory bank failed: {r.message}")


# ================================================================
# 3. Runner
# ================================================================

class TestRunner(unittest.TestCase):
    def test_run_all_returns_list(self):
        results = run_all(no_color=True, stream=io.StringIO())
        self.assertIsInstance(results, list)
        self.assertEqual(len(results), len(ALL_CHECKS))

    def test_run_all_all_have_timing(self):
        results = run_all(no_color=True, stream=io.StringIO())
        for r in results:
            self.assertIsInstance(r, CheckResult)
            self.assertGreater(r.elapsed_ms, 0)

    def test_exit_code_all_pass(self):
        results = [
            CheckResult(name="a", status=CheckStatus.PASS, message="ok"),
            CheckResult(name="b", status=CheckStatus.PASS, message="ok"),
        ]
        self.assertEqual(exit_code(results), 0)

    def test_exit_code_with_warn(self):
        results = [
            CheckResult(name="a", status=CheckStatus.PASS, message="ok"),
            CheckResult(name="b", status=CheckStatus.WARN, message="hmm"),
        ]
        self.assertEqual(exit_code(results), 0)

    def test_exit_code_with_fail(self):
        results = [
            CheckResult(name="a", status=CheckStatus.PASS, message="ok"),
            CheckResult(name="b", status=CheckStatus.FAIL, message="bad"),
        ]
        self.assertEqual(exit_code(results), 1)


# ================================================================
# 4. Module integrity
# ================================================================

class TestModuleIntegrity(unittest.TestCase):
    def test_all_checks_list(self):
        self.assertEqual(len(ALL_CHECKS), 10)
        # Each element should be callable
        for fn in ALL_CHECKS:
            self.assertTrue(callable(fn))

    def test_init_exports(self):
        import fw_doctor
        # Key exports must be present
        self.assertTrue(hasattr(fw_doctor, "ALL_CHECKS"))
        self.assertTrue(hasattr(fw_doctor, "CheckResult"))
        self.assertTrue(hasattr(fw_doctor, "CheckStatus"))
        self.assertTrue(hasattr(fw_doctor, "run_all"))
        self.assertTrue(hasattr(fw_doctor, "exit_code"))

    def test_main_module(self):
        """__main__.py should define a main function."""
        from fw_doctor.__main__ import main
        self.assertTrue(callable(main))


# ================================================================
# 5. Integration — full pipeline
# ================================================================

class TestDoctorIntegration(unittest.TestCase):
    """End-to-end: run all checks and verify the gate verdict."""

    def test_full_pipeline_no_catastrophic_failure(self):
        """All checks should run without uncaught exceptions."""
        buf = io.StringIO()
        results = run_all(no_color=True, stream=buf)
        # At minimum, no check should throw an unhandled exception
        for r in results:
            self.assertIsInstance(r, CheckResult)
            self.assertIsInstance(r.status, CheckStatus)

    def test_full_pipeline_gate(self):
        """AX52: if any check FAILs, exit code should be 1."""
        buf = io.StringIO()
        results = run_all(no_color=True, stream=buf)
        code = exit_code(results)
        fails = [r for r in results if r.status == CheckStatus.FAIL]
        if fails:
            self.assertEqual(code, 1)
        else:
            self.assertEqual(code, 0)

    def test_output_contains_banner(self):
        """Runner should produce readable output."""
        buf = io.StringIO()
        run_all(no_color=True, stream=buf)
        output = buf.getvalue()
        self.assertIn("fw_doctor", output.lower())


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