"""Tests for DentalComplianceOS frameworks, store, scanners, and server."""

from __future__ import annotations

import json
import os
import tempfile
from pathlib import Path

import pytest

from dentalcomplianceos.frameworks import (
    FRAMEWORKS,
    FRAMEWORK_NAMES,
    get_all_checks,
    get_checks_for_practice_type,
    get_checks_for_state,
)
from dentalcomplianceos.frameworks.osha_dental import OSHA_DENTAL_CONTROLS
from dentalcomplianceos.frameworks.cdc_infection import CDC_INFECTION_CONTROLS
from dentalcomplianceos.frameworks.hipaa_dental import HIPAA_DENTAL_CONTROLS
from dentalcomplianceos.frameworks.radiation_safety import RADIATION_SAFETY_CONTROLS
from dentalcomplianceos.frameworks.state_boards import STATE_BOARD_CONTROLS
from dentalcomplianceos.frameworks.sterilization import STERILIZATION_CONTROLS
from dentalcomplianceos.store import DentalComplianceStore
from dentalcomplianceos.types import (
    ComplianceCheck,
    ComplianceFinding,
    ComplianceStatus,
    Severity,
    ScanResult,
    SterilizationRecord,
    RadiationRecord,
    PracticeType,
    CycleType,
)
from dentalcomplianceos.scanners.practice_scanner import PracticeScanner
from dentalcomplianceos.scanners.checklist_scanner import ChecklistScanner
from dentalcomplianceos.server import _run_scan, _generate_corrective_actions, _generate_inspection_checklist


# ── Framework Tests ──


class TestFrameworks:
    """Test that all frameworks are properly structured."""

    def test_all_frameworks_registered(self) -> None:
        """All 6 frameworks should be registered."""
        assert len(FRAMEWORKS) == 6
        expected = {"osha_dental", "cdc_infection", "hipaa_dental",
                     "radiation_safety", "state_boards", "sterilization"}
        assert set(FRAMEWORKS.keys()) == expected

    def test_all_frameworks_have_names(self) -> None:
        """Every framework key should have a display name."""
        for key in FRAMEWORKS:
            assert key in FRAMEWORK_NAMES
            assert len(FRAMEWORK_NAMES[key]) > 0

    def test_osha_dental_has_50_plus_checks(self) -> None:
        """OSHA dental should have 50+ checks."""
        checks = get_all_checks("osha_dental")
        assert len(checks) >= 50, f"Expected 50+ OSHA checks, got {len(checks)}"

    def test_cdc_infection_has_40_plus_checks(self) -> None:
        """CDC infection control should have 40+ checks."""
        checks = get_all_checks("cdc_infection")
        assert len(checks) >= 40, f"Expected 40+ CDC checks, got {len(checks)}"

    def test_hipaa_dental_has_30_plus_checks(self) -> None:
        """HIPAA dental should have 30+ checks."""
        checks = get_all_checks("hipaa_dental")
        assert len(checks) >= 30, f"Expected 30+ HIPAA checks, got {len(checks)}"

    def test_radiation_safety_has_25_plus_checks(self) -> None:
        """Radiation safety should have 25+ checks."""
        checks = get_all_checks("radiation_safety")
        assert len(checks) >= 25, f"Expected 25+ radiation checks, got {len(checks)}"

    def test_state_boards_has_20_plus_checks(self) -> None:
        """State boards should have 20+ checks."""
        checks = get_all_checks("state_boards")
        assert len(checks) >= 20, f"Expected 20+ state board checks, got {len(checks)}"

    def test_sterilization_has_20_plus_checks(self) -> None:
        """Sterilization should have 20+ checks."""
        checks = get_all_checks("sterilization")
        assert len(checks) >= 20, f"Expected 20+ sterilization checks, got {len(checks)}"

    def test_every_check_has_required_fields(self) -> None:
        """Every check should have id, check, severity, remediation."""
        for fw_key in FRAMEWORKS:
            checks = get_all_checks(fw_key)
            for check in checks:
                assert "id" in check, f"Missing 'id' in {fw_key} check"
                assert "check" in check, f"Missing 'check' in {fw_key}/{check.get('id', '?')}"
                assert "severity" in check, f"Missing 'severity' in {fw_key}/{check['id']}"
                assert "remediation" in check, f"Missing 'remediation' in {fw_key}/{check['id']}"
                assert check["severity"] in ("critical", "high", "medium", "low", "info"), \
                    f"Invalid severity '{check['severity']}' in {fw_key}/{check['id']}"

    def test_every_check_has_regulation_reference(self) -> None:
        """Every control should have a regulation_reference."""
        for fw_key in FRAMEWORKS:
            controls = FRAMEWORKS[fw_key]
            for control_id, control in controls.items():
                assert "regulation_reference" in control, \
                    f"Missing regulation_reference in {fw_key}/{control_id}"

    def test_check_ids_unique_within_framework(self) -> None:
        """Check IDs should be unique within each framework."""
        for fw_key in FRAMEWORKS:
            checks = get_all_checks(fw_key)
            ids = [c["id"] for c in checks]
            assert len(ids) == len(set(ids)), \
                f"Duplicate check IDs in {fw_key}: {[x for x in ids if ids.count(x) > 1]}"

    def test_get_checks_for_practice_type(self) -> None:
        """Practice type filtering should work."""
        all_checks = get_all_checks("osha_dental")
        gp_checks = get_checks_for_practice_type("osha_dental", "solo_gp")
        # All OSHA checks apply to all practice types
        assert len(gp_checks) == len(all_checks)

    def test_get_checks_for_state(self) -> None:
        """State filtering should work for state_boards framework."""
        all_checks = get_all_checks("state_boards")
        ca_checks = get_checks_for_state("state_boards", "CA")
        # CA checks should include checks with no state filter + CA-specific
        assert len(ca_checks) > 0
        assert len(ca_checks) <= len(all_checks)


# ── Type Tests ──


class TestTypes:
    """Test core data types."""

    def test_practice_type_enum(self) -> None:
        """PracticeType enum should have expected values."""
        assert PracticeType.SOLO_GP.value == "solo_gp"
        assert PracticeType.ORAL_SURGERY.value == "oral_surgery"
        assert PracticeType.DSO.value == "dso"

    def test_cycle_type_enum(self) -> None:
        """CycleType enum should have expected values."""
        assert CycleType.GRAVITY.value == "gravity"
        assert CycleType.PRE_VACUUM.value == "pre_vacuum"
        assert CycleType.FLASH.value == "flash"

    def test_compliance_finding_to_dict(self) -> None:
        """ComplianceFinding should serialize properly."""
        finding = ComplianceFinding(
            framework="osha_dental",
            check_id="BBP-1.1",
            title="Exposure Control Plan",
            status=ComplianceStatus.NON_COMPLIANT,
            severity=Severity.CRITICAL,
            detail="No evidence found",
        )
        d = finding.to_dict()
        assert d["status"] == "non_compliant"
        assert d["severity"] == "critical"
        assert d["check_id"] == "BBP-1.1"

    def test_sterilization_record_to_dict(self) -> None:
        """SterilizationRecord should serialize."""
        record = SterilizationRecord(
            autoclave_id="Statim-1",
            cycle_type="gravity",
            temperature=250.0,
            pressure=15.0,
            duration_minutes=30.0,
            biological_indicator="pass",
            chemical_indicator="pass",
        )
        d = record.to_dict()
        assert d["autoclave_id"] == "Statim-1"
        assert d["temperature"] == 250.0

    def test_scan_result_to_dict(self) -> None:
        """ScanResult should serialize with findings."""
        result = ScanResult(
            scan_id="test-123",
            framework="osha_dental",
            practice_type="solo_gp",
            state="CA",
            timestamp="2026-01-01T00:00:00",
            total_checks=10,
            compliant=8,
            non_compliant=2,
            score=80.0,
            findings=[
                ComplianceFinding(
                    framework="osha_dental",
                    check_id="BBP-1.1",
                    title="Test",
                    status=ComplianceStatus.NON_COMPLIANT,
                    severity=Severity.HIGH,
                ),
            ],
        )
        d = result.to_dict()
        assert d["scan_id"] == "test-123"
        assert d["score"] == 80.0
        assert len(d["findings"]) == 1
        assert d["findings"][0]["status"] == "non_compliant"


# ── Store Tests ──


class TestStore:
    """Test SQLite store operations."""

    def _make_store(self, tmp_path: Path) -> DentalComplianceStore:
        """Create a store with a temp database."""
        return DentalComplianceStore(tmp_path / "test.db")

    def test_store_creates_tables(self, tmp_path: Path) -> None:
        """Store should create tables on first access."""
        store = self._make_store(tmp_path)
        # Access conn to trigger table creation
        assert store.conn is not None
        tables = store.conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table'"
        ).fetchall()
        table_names = {t["name"] for t in tables}
        assert "compliance_status" in table_names
        assert "sterilization_log" in table_names
        assert "radiation_log" in table_names
        assert "training_log" in table_names
        assert "scans" in table_names
        assert "scan_findings" in table_names
        store.close()

    def test_log_sterilization(self, tmp_path: Path) -> None:
        """Should log sterilization cycles."""
        store = self._make_store(tmp_path)
        record = SterilizationRecord(
            autoclave_id="Statim-1",
            cycle_type="gravity",
            temperature=250.0,
            pressure=15.0,
            duration_minutes=30.0,
            biological_indicator="pass",
            chemical_indicator="pass",
            load_contents="Extraction kit",
            operator="Jane",
        )
        log_id = store.log_sterilization(record)
        assert log_id > 0

        logs = store.get_sterilization_log(days=1)
        assert len(logs) == 1
        assert logs[0]["autoclave_id"] == "Statim-1"
        assert logs[0]["operator"] == "Jane"
        store.close()

    def test_log_radiation(self, tmp_path: Path) -> None:
        """Should log radiation events."""
        store = self._make_store(tmp_path)
        record = RadiationRecord(
            equipment_id="Dexis-Op1",
            log_type="qa_test",
            details="Annual kVp accuracy test",
            result="pass",
            operator="Dr. Smith",
        )
        log_id = store.log_radiation(record)
        assert log_id > 0

        logs = store.get_radiation_log(days=1)
        assert len(logs) == 1
        assert logs[0]["equipment_id"] == "Dexis-Op1"
        store.close()

    def test_save_and_retrieve_scan(self, tmp_path: Path) -> None:
        """Should save and retrieve scan results."""
        store = self._make_store(tmp_path)
        result = ScanResult(
            scan_id="dental-scan-abc123",
            framework="osha_dental",
            practice_type="solo_gp",
            state="CA",
            timestamp="2026-01-01T00:00:00",
            total_checks=50,
            compliant=45,
            non_compliant=5,
            score=90.0,
            findings=[
                ComplianceFinding(
                    framework="osha_dental",
                    check_id="BBP-1.1",
                    title="Exposure Control Plan",
                    status=ComplianceStatus.NON_COMPLIANT,
                    severity=Severity.CRITICAL,
                    detail="No evidence found",
                ),
            ],
        )
        store.save_scan(result)

        latest = store.get_latest_scan("osha_dental")
        assert latest is not None
        assert latest["scan_id"] == "dental-scan-abc123"
        assert latest["score"] == 90.0
        assert len(latest["findings"]) == 1
        store.close()

    def test_get_non_compliant_findings(self, tmp_path: Path) -> None:
        """Should retrieve non-compliant findings."""
        store = self._make_store(tmp_path)
        result = ScanResult(
            scan_id="dental-scan-def456",
            framework="cdc_infection",
            practice_type="group",
            state="TX",
            timestamp="2026-01-01T00:00:00",
            total_checks=40,
            compliant=35,
            non_compliant=5,
            score=87.5,
            findings=[
                ComplianceFinding(
                    framework="cdc_infection",
                    check_id="HH-1.1",
                    title="Hand hygiene",
                    status=ComplianceStatus.NON_COMPLIANT,
                    severity=Severity.CRITICAL,
                ),
                ComplianceFinding(
                    framework="cdc_infection",
                    check_id="SURF-1.1",
                    title="Surface disinfection",
                    status=ComplianceStatus.NON_COMPLIANT,
                    severity=Severity.HIGH,
                ),
            ],
        )
        store.save_scan(result)

        findings = store.get_non_compliant_findings("cdc_infection")
        assert len(findings) == 2
        # Should be ordered by severity (critical first)
        assert findings[0]["severity"] == "critical"
        store.close()

    def test_log_training(self, tmp_path: Path) -> None:
        """Should log training records."""
        store = self._make_store(tmp_path)
        log_id = store.log_training(
            employee="Jane Doe",
            training_type="BBP Annual",
            completion_date="2026-01-15",
            expiration_date="2027-01-15",
            certificate="CERT-001",
        )
        assert log_id > 0

        logs = store.get_training_log(employee="Jane Doe")
        assert len(logs) == 1
        assert logs[0]["training_type"] == "BBP Annual"
        store.close()

    def test_get_last_spore_test(self, tmp_path: Path) -> None:
        """Should find the last spore test for an autoclave."""
        store = self._make_store(tmp_path)
        store.log_sterilization(SterilizationRecord(
            autoclave_id="M11",
            cycle_type="pre_vacuum",
            temperature=270.0,
            pressure=30.0,
            duration_minutes=4.0,
            biological_indicator="pass",
            chemical_indicator="pass",
        ))
        last = store.get_last_spore_test("M11")
        assert last is not None
        assert last["biological_indicator"] == "pass"
        store.close()

    def test_compliance_status_update(self, tmp_path: Path) -> None:
        """Should update and retrieve compliance status."""
        store = self._make_store(tmp_path)
        store.update_compliance_status(
            framework="osha_dental",
            check_id="BBP-1.1",
            status="compliant",
            evidence="Exposure Control Plan v3.2 on file",
        )
        statuses = store.get_compliance_status("osha_dental")
        assert len(statuses) == 1
        assert statuses[0]["status"] == "compliant"
        store.close()


# ── Scanner Tests ──


class TestPracticeScanner:
    """Test practice document scanner."""

    def test_scan_empty_directory(self, tmp_path: Path) -> None:
        """Empty directory should yield all non-compliant."""
        scanner = PracticeScanner(tmp_path)
        checks = [
            {
                "id": "BBP-1.1",
                "check": "Exposure Control Plan exists",
                "framework": "osha_dental",
                "severity": "critical",
                "remediation": "Create plan.",
            },
        ]
        findings = scanner.scan(checks)
        assert len(findings) == 1
        assert findings[0].status == ComplianceStatus.NON_COMPLIANT

    def test_scan_finds_evidence(self, tmp_path: Path) -> None:
        """Should find evidence files and mark checks compliant."""
        # Create evidence file
        (tmp_path / "exposure_control_plan.pdf").touch()
        scanner = PracticeScanner(tmp_path)
        checks = [
            {
                "id": "BBP-1.1",
                "check": "Exposure Control Plan exists",
                "framework": "osha_dental",
                "severity": "critical",
                "remediation": "Create plan.",
            },
        ]
        findings = scanner.scan(checks)
        # BBP-1.1 is in the evidence map for exposure_control_plan
        assert len(findings) == 0  # Should be compliant

    def test_scan_with_compliance_status_json(self, tmp_path: Path) -> None:
        """Should read compliance_status.json for check results."""
        status_data = {
            "checks": [
                {"check_id": "CUSTOM-1", "status": "compliant"},
            ]
        }
        (tmp_path / "compliance_status.json").write_text(
            json.dumps(status_data), encoding="utf-8"
        )
        scanner = PracticeScanner(tmp_path)
        checks = [
            {
                "id": "CUSTOM-1",
                "check": "Custom check",
                "framework": "test",
                "severity": "medium",
                "remediation": "Fix it.",
            },
        ]
        findings = scanner.scan(checks)
        assert len(findings) == 0  # Satisfied by JSON


class TestChecklistScanner:
    """Test checklist scanner."""

    def test_scan_with_checklist(self, tmp_path: Path) -> None:
        """Should read checklist files and evaluate."""
        checklist = {
            "items": [
                {"check_id": "BBP-1.1", "status": "compliant"},
                {"check_id": "BBP-1.2", "status": "non_compliant"},
            ]
        }
        (tmp_path / "compliance_checklist.json").write_text(
            json.dumps(checklist), encoding="utf-8"
        )
        scanner = ChecklistScanner(tmp_path)
        checks = [
            {"id": "BBP-1.1", "check": "Check 1", "framework": "osha_dental",
             "severity": "critical", "remediation": "Fix."},
            {"id": "BBP-1.2", "check": "Check 2", "framework": "osha_dental",
             "severity": "high", "remediation": "Fix."},
        ]
        findings = scanner.scan(checks)
        assert len(findings) == 1
        assert findings[0].check_id == "BBP-1.2"
        assert findings[0].status == ComplianceStatus.NON_COMPLIANT

    def test_scan_missing_checklist(self, tmp_path: Path) -> None:
        """Missing checklist should yield unknown status."""
        scanner = ChecklistScanner(tmp_path)
        checks = [
            {"id": "TEST-1", "check": "Test", "framework": "test",
             "severity": "medium", "remediation": "Fix."},
        ]
        findings = scanner.scan(checks)
        assert len(findings) == 1
        assert findings[0].status == ComplianceStatus.UNKNOWN


# ── Server Logic Tests ──


class TestServerLogic:
    """Test server helper functions."""

    def test_run_scan(self, tmp_path: Path) -> None:
        """_run_scan should return structured result."""
        store = DentalComplianceStore(tmp_path / "test.db")
        result = _run_scan(str(tmp_path), "osha_dental", store, "solo_gp", "CA")
        assert "error" not in result
        assert result["framework"] == "osha_dental"
        assert result["total_checks"] >= 50
        assert "score" in result
        assert "findings" in result
        store.close()

    def test_run_scan_invalid_framework(self, tmp_path: Path) -> None:
        """Invalid framework should return error."""
        store = DentalComplianceStore(tmp_path / "test.db")
        result = _run_scan(str(tmp_path), "nonexistent", store)
        assert "error" in result
        store.close()

    def test_run_scan_invalid_path(self, tmp_path: Path) -> None:
        """Non-existent path should return error."""
        store = DentalComplianceStore(tmp_path / "test.db")
        result = _run_scan("/nonexistent/path/abc", "osha_dental", store)
        assert "error" in result
        store.close()

    def test_generate_corrective_actions(self) -> None:
        """Should generate prioritized corrective actions."""
        findings = [
            {"check_id": "BBP-1.1", "title": "ECP", "severity": "critical",
             "status": "non_compliant", "corrective_action": "Create plan"},
            {"check_id": "PPE-1.1", "title": "PPE", "severity": "medium",
             "status": "non_compliant", "corrective_action": "Assess hazards"},
        ]
        actions = _generate_corrective_actions(findings)
        assert len(actions) == 2
        assert actions[0]["priority"] == 1  # Critical first
        assert actions[1]["priority"] == 3  # Medium second

    def test_generate_inspection_checklist(self) -> None:
        """Should generate checklist for state and practice type."""
        checklist = _generate_inspection_checklist("CA", "solo_gp")
        assert checklist["state"] == "CA"
        assert checklist["practice_type"] == "solo_gp"
        assert checklist["total_items"] > 100  # Should have many items across all frameworks
        assert "OSHA Dental Standards" in checklist["frameworks"]

    def test_run_scan_with_evidence(self, tmp_path: Path) -> None:
        """Scan with evidence files should have higher compliance score."""
        # Create some evidence files
        (tmp_path / "exposure_control_plan.docx").touch()
        (tmp_path / "hazard_communication_program.pdf").touch()
        (tmp_path / "training_records").mkdir()
        (tmp_path / "training_records" / "bbp_training_2026.pdf").touch()
        (tmp_path / "sds_binder").mkdir()
        (tmp_path / "sds_binder" / "chemical_sds.pdf").touch()

        store = DentalComplianceStore(tmp_path / "test.db")
        result_with_evidence = _run_scan(str(tmp_path), "osha_dental", store, "solo_gp")

        # Create empty directory for comparison
        empty_dir = tmp_path / "empty"
        empty_dir.mkdir()
        result_empty = _run_scan(str(empty_dir), "osha_dental", store, "solo_gp")

        # Score with evidence should be higher (or equal in edge cases)
        assert result_with_evidence["score"] >= result_empty["score"]
        assert result_with_evidence["non_compliant"] <= result_empty["non_compliant"]
        store.close()


# ── Integration Tests ──


class TestIntegration:
    """End-to-end integration tests."""

    def test_full_workflow(self, tmp_path: Path) -> None:
        """Test scan -> fix -> report workflow."""
        store = DentalComplianceStore(tmp_path / "test.db")

        # 1. Scan
        result = _run_scan(str(tmp_path), "sterilization", store, "solo_gp")
        assert "error" not in result
        assert result["total_checks"] >= 20

        # 2. Log sterilization
        record = SterilizationRecord(
            autoclave_id="Test-Auto",
            cycle_type="gravity",
            temperature=250.0,
            pressure=15.0,
            duration_minutes=30.0,
            biological_indicator="pass",
            chemical_indicator="pass",
        )
        log_id = store.log_sterilization(record)
        assert log_id > 0

        # 3. Log radiation
        rad = RadiationRecord(
            equipment_id="Test-Xray",
            log_type="qa_test",
            details="Annual test",
            result="pass",
        )
        store.log_radiation(rad)

        # 4. Generate corrective actions
        findings = store.get_non_compliant_findings("sterilization")
        actions = _generate_corrective_actions(findings)
        assert len(actions) > 0

        # 5. Generate report
        from dentalcomplianceos.server import _generate_report
        report_md = _generate_report(store, "sterilization", "markdown")
        assert "DentalComplianceOS Audit Report" in report_md
        assert "Sterilization Protocols" in report_md

        report_json = _generate_report(store, "sterilization", "json")
        data = json.loads(report_json)
        assert "sterilization" in data["frameworks"]

        store.close()

    def test_multi_framework_scan(self, tmp_path: Path) -> None:
        """Should be able to scan multiple frameworks."""
        store = DentalComplianceStore(tmp_path / "test.db")

        for fw in FRAMEWORKS:
            result = _run_scan(str(tmp_path), fw, store, "solo_gp", "CA")
            assert "error" not in result
            assert result["framework"] == fw
            assert result["total_checks"] > 0

        summaries = store.get_all_scan_summaries()
        assert len(summaries) == 6
        store.close()
