"""
Unit tests for ISMS (Information Security Management System) API Routes.

Tests all ISO 27001 certification-related endpoints:
- ISMS Scope (Chapter 4.3)
- ISMS Policies (Chapter 5.2)
- Security Objectives (Chapter 6.2)
- Statement of Applicability (SoA)
- Audit Findings & CAPA
- Management Reviews (Chapter 9.3)
- Internal Audits (Chapter 9.2)
- Readiness Check

Run with: pytest backend/compliance/tests/test_isms_routes.py -v
"""

import pytest
from datetime import datetime, date, timezone
from unittest.mock import MagicMock
from uuid import uuid4

from sqlalchemy.orm import Session

import sys
sys.path.insert(0, '/Users/benjaminadmin/Projekte/breakpilot-pwa/backend')

from compliance.db.models import (
    ISMSScopeDB, ISMSPolicyDB, SecurityObjectiveDB,
    StatementOfApplicabilityDB, AuditFindingDB, CorrectiveActionDB,
    ManagementReviewDB, InternalAuditDB, AuditTrailDB, ISMSReadinessCheckDB,
    ApprovalStatusEnum, FindingTypeEnum, FindingStatusEnum, CAPATypeEnum
)


# ============================================================================
# Test Fixtures
# ============================================================================

@pytest.fixture
def mock_db():
    """Create a mock database session."""
    return MagicMock(spec=Session)


@pytest.fixture
def sample_scope():
    """Create a sample ISMS scope for testing."""
    return ISMSScopeDB(
        id=str(uuid4()),
        scope_statement="BreakPilot ISMS covers all digital learning platform operations",
        included_locations=["Frankfurt Office", "AWS eu-central-1"],
        included_processes=["Software Development", "Data Processing", "Customer Support"],
        included_services=["BreakPilot PWA", "Consent Service", "AI Assistant"],
        excluded_items=["Marketing Website"],
        exclusion_justification="Marketing website is static and contains no user data",
        status=ApprovalStatusEnum.DRAFT,
        version="1.0",
        created_by="admin@breakpilot.de",
        created_at=datetime.now(timezone.utc),
    )


@pytest.fixture
def sample_approved_scope(sample_scope):
    """Create an approved ISMS scope for testing."""
    sample_scope.status = ApprovalStatusEnum.APPROVED
    sample_scope.approved_by = "ceo@breakpilot.de"
    sample_scope.approved_at = datetime.now(timezone.utc)
    sample_scope.effective_date = date.today()
    sample_scope.review_date = date(date.today().year + 1, date.today().month, date.today().day)
    sample_scope.approval_signature = "sha256_signature_hash"
    return sample_scope


@pytest.fixture
def sample_policy():
    """Create a sample ISMS policy for testing."""
    return ISMSPolicyDB(
        id=str(uuid4()),
        policy_id="POL-ISMS-001",
        title="Information Security Policy",
        policy_type="master",
        description="Master ISMS policy for BreakPilot",
        policy_text="This policy establishes the framework for information security...",
        applies_to=["All Employees", "Contractors", "Partners"],
        review_frequency_months=12,
        related_controls=["GOV-001", "GOV-002"],
        authored_by="iso@breakpilot.de",
        status=ApprovalStatusEnum.DRAFT,
        version="1.0",
        created_at=datetime.now(timezone.utc),
    )


@pytest.fixture
def sample_objective():
    """Create a sample security objective for testing."""
    return SecurityObjectiveDB(
        id=str(uuid4()),
        objective_id="OBJ-2026-001",
        title="Reduce Security Incidents",
        description="Reduce the number of security incidents by 30% compared to previous year",
        category="operational",
        specific="Reduce security incidents from 10 to 7 per year",
        measurable="Number of security incidents recorded in ticketing system",
        achievable="Based on trend analysis and planned control improvements",
        relevant="Directly supports information security goals",
        time_bound="By end of Q4 2026",
        kpi_name="Security Incident Count",
        kpi_target=7.0,
        kpi_unit="incidents/year",
        kpi_current=10.0,
        measurement_frequency="monthly",
        owner="security@breakpilot.de",
        target_date=date(2026, 12, 31),
        related_controls=["OPS-003"],
        status="active",
        progress_percentage=0.0,
        created_at=datetime.now(timezone.utc),
    )


@pytest.fixture
def sample_soa_entry():
    """Create a sample SoA entry for testing."""
    return StatementOfApplicabilityDB(
        id=str(uuid4()),
        annex_a_control="A.5.1",
        annex_a_title="Policies for information security",
        annex_a_category="organizational",
        is_applicable=True,
        applicability_justification="Required for ISMS governance",
        implementation_status="implemented",
        implementation_notes="Implemented via GOV-001, GOV-002 controls",
        breakpilot_control_ids=["GOV-001", "GOV-002"],
        coverage_level="full",
        evidence_description="ISMS Policy v2.0, signed by CEO",
        version="1.0",
        created_at=datetime.now(timezone.utc),
    )


@pytest.fixture
def sample_finding():
    """Create a sample audit finding for testing."""
    return AuditFindingDB(
        id=str(uuid4()),
        finding_id="FIND-2026-001",
        finding_type=FindingTypeEnum.MINOR,
        iso_chapter="9.2",
        annex_a_control="A.5.35",
        title="Internal audit schedule not documented",
        description="The internal audit schedule for 2026 was not formally documented",
        objective_evidence="No document found in DMS",
        impact_description="Cannot demonstrate planned approach to internal audits",
        owner="iso@breakpilot.de",
        auditor="external.auditor@cert.de",
        identified_date=date.today(),
        due_date=date(2026, 3, 31),
        status=FindingStatusEnum.OPEN,
        created_at=datetime.now(timezone.utc),
    )


@pytest.fixture
def sample_major_finding():
    """Create a major finding (certification blocking)."""
    return AuditFindingDB(
        id=str(uuid4()),
        finding_id="FIND-2026-002",
        finding_type=FindingTypeEnum.MAJOR,
        iso_chapter="5.2",
        title="Information Security Policy not approved",
        description="The ISMS master policy has not been approved by top management",
        objective_evidence="Policy document shows 'Draft' status",
        owner="ceo@breakpilot.de",
        auditor="external.auditor@cert.de",
        identified_date=date.today(),
        due_date=date(2026, 2, 28),
        status=FindingStatusEnum.OPEN,
        created_at=datetime.now(timezone.utc),
    )


@pytest.fixture
def sample_capa(sample_finding):
    """Create a sample CAPA for testing."""
    return CorrectiveActionDB(
        id=str(uuid4()),
        capa_id="CAPA-2026-001",
        finding_id=sample_finding.id,
        capa_type=CAPATypeEnum.CORRECTIVE,
        title="Create and approve internal audit schedule",
        description="Create a formal internal audit schedule document and get management approval",
        expected_outcome="Approved internal audit schedule for 2026",
        assigned_to="iso@breakpilot.de",
        planned_start=date.today(),
        planned_completion=date(2026, 2, 15),
        effectiveness_criteria="Document approved and distributed to audit team",
        status="planned",
        created_at=datetime.now(timezone.utc),
    )


@pytest.fixture
def sample_management_review():
    """Create a sample management review for testing."""
    return ManagementReviewDB(
        id=str(uuid4()),
        review_id="MR-2026-Q1",
        title="Q1 2026 Management Review",
        review_date=date(2026, 1, 15),
        review_period_start=date(2025, 10, 1),
        review_period_end=date(2025, 12, 31),
        chairperson="ceo@breakpilot.de",
        attendees=[
            {"name": "CEO", "role": "Chairperson"},
            {"name": "CTO", "role": "Technical Lead"},
            {"name": "ISO", "role": "ISMS Manager"},
        ],
        status="draft",
        created_at=datetime.now(timezone.utc),
    )


@pytest.fixture
def sample_internal_audit():
    """Create a sample internal audit for testing."""
    return InternalAuditDB(
        id=str(uuid4()),
        audit_id="IA-2026-001",
        title="Annual ISMS Internal Audit 2026",
        audit_type="full",
        scope_description="Complete ISMS audit covering all ISO 27001 chapters and Annex A controls",
        iso_chapters_covered=["4", "5", "6", "7", "8", "9", "10"],
        annex_a_controls_covered=["A.5", "A.6", "A.7", "A.8"],
        criteria="ISO 27001:2022, Internal ISMS procedures",
        planned_date=date(2026, 3, 1),
        lead_auditor="internal.auditor@breakpilot.de",
        audit_team=["internal.auditor@breakpilot.de", "qa@breakpilot.de"],
        status="planned",
        created_at=datetime.now(timezone.utc),
    )


# ============================================================================
# Test: ISMS Scope
# ============================================================================

class TestISMSScope:
    """Tests for ISMS Scope endpoints."""

    def test_scope_has_required_fields(self, sample_scope):
        """ISMS scope should have all required fields."""
        assert sample_scope.scope_statement is not None
        assert sample_scope.status == ApprovalStatusEnum.DRAFT
        assert sample_scope.created_by is not None

    def test_scope_approval_sets_correct_fields(self, sample_approved_scope):
        """Approving scope should set approval fields."""
        assert sample_approved_scope.status == ApprovalStatusEnum.APPROVED
        assert sample_approved_scope.approved_by is not None
        assert sample_approved_scope.approved_at is not None
        assert sample_approved_scope.effective_date is not None
        assert sample_approved_scope.review_date is not None
        assert sample_approved_scope.approval_signature is not None

    def test_scope_can_include_multiple_locations(self, sample_scope):
        """Scope should support multiple locations."""
        assert isinstance(sample_scope.included_locations, list)
        assert len(sample_scope.included_locations) >= 1

    def test_scope_exclusions_require_justification(self, sample_scope):
        """Scope exclusions should have justification (ISO 27001 requirement)."""
        if sample_scope.excluded_items:
            assert sample_scope.exclusion_justification is not None


# ============================================================================
# Test: ISMS Policies
# ============================================================================

class TestISMSPolicy:
    """Tests for ISMS Policy endpoints."""

    def test_policy_has_unique_id(self, sample_policy):
        """Each policy should have a unique policy_id."""
        assert sample_policy.policy_id is not None
        assert sample_policy.policy_id.startswith("POL-")

    def test_master_policy_type_exists(self, sample_policy):
        """Master policy type should be 'master'."""
        assert sample_policy.policy_type == "master"

    def test_policy_has_review_frequency(self, sample_policy):
        """Policy should specify review frequency."""
        assert sample_policy.review_frequency_months > 0
        assert sample_policy.review_frequency_months <= 36  # Max 3 years

    def test_policy_can_link_to_controls(self, sample_policy):
        """Policy should link to related controls."""
        assert isinstance(sample_policy.related_controls, list)


# ============================================================================
# Test: Security Objectives
# ============================================================================

class TestSecurityObjective:
    """Tests for Security Objectives endpoints."""

    def test_objective_follows_smart_criteria(self, sample_objective):
        """Objectives should follow SMART criteria."""
        # S - Specific
        assert sample_objective.specific is not None
        # M - Measurable
        assert sample_objective.measurable is not None
        # A - Achievable
        assert sample_objective.achievable is not None
        # R - Relevant
        assert sample_objective.relevant is not None
        # T - Time-bound
        assert sample_objective.time_bound is not None

    def test_objective_has_kpi(self, sample_objective):
        """Objectives should have measurable KPIs."""
        assert sample_objective.kpi_name is not None
        assert sample_objective.kpi_target is not None
        assert sample_objective.kpi_unit is not None

    def test_objective_progress_calculation(self, sample_objective):
        """Objective progress should be calculable."""
        if sample_objective.kpi_target and sample_objective.kpi_current:
            # Progress towards reducing incidents (lower is better for this KPI)
            expected_progress = max(0, min(100,
                (sample_objective.kpi_target / sample_objective.kpi_current) * 100
            ))
            assert expected_progress >= 0
            assert expected_progress <= 100


# ============================================================================
# Test: Statement of Applicability (SoA)
# ============================================================================

class TestStatementOfApplicability:
    """Tests for SoA endpoints."""

    def test_soa_entry_has_annex_a_reference(self, sample_soa_entry):
        """SoA entry should reference Annex A control."""
        assert sample_soa_entry.annex_a_control is not None
        assert sample_soa_entry.annex_a_control.startswith("A.")

    def test_soa_entry_requires_justification_for_not_applicable(self):
        """Non-applicable controls must have justification."""
        soa_entry = StatementOfApplicabilityDB(
            id=str(uuid4()),
            annex_a_control="A.7.2",
            annex_a_title="Physical entry",
            annex_a_category="physical",
            is_applicable=False,
            applicability_justification="Cloud-only infrastructure, no physical data center",
        )
        assert not soa_entry.is_applicable
        assert soa_entry.applicability_justification is not None

    def test_soa_entry_tracks_implementation_status(self, sample_soa_entry):
        """SoA should track implementation status."""
        valid_statuses = ["planned", "in_progress", "implemented", "not_implemented"]
        assert sample_soa_entry.implementation_status in valid_statuses

    def test_soa_entry_maps_to_breakpilot_controls(self, sample_soa_entry):
        """SoA should map Annex A controls to Breakpilot controls."""
        assert isinstance(sample_soa_entry.breakpilot_control_ids, list)


# ============================================================================
# Test: Audit Findings
# ============================================================================

class TestAuditFinding:
    """Tests for Audit Finding endpoints."""

    def test_finding_has_classification(self, sample_finding):
        """Finding should have Major/Minor/OFI classification."""
        valid_types = [FindingTypeEnum.MAJOR, FindingTypeEnum.MINOR,
                       FindingTypeEnum.OFI, FindingTypeEnum.POSITIVE]
        assert sample_finding.finding_type in valid_types

    def test_major_finding_blocks_certification(self, sample_major_finding):
        """Major findings should be identified as certification blocking."""
        assert sample_major_finding.finding_type == FindingTypeEnum.MAJOR
        # is_blocking is a property, so we check the type
        is_blocking = (sample_major_finding.finding_type == FindingTypeEnum.MAJOR and
                       sample_major_finding.status != FindingStatusEnum.CLOSED)
        assert is_blocking

    def test_finding_has_objective_evidence(self, sample_finding):
        """Findings should have objective evidence."""
        assert sample_finding.objective_evidence is not None

    def test_finding_has_due_date(self, sample_finding):
        """Findings should have a due date for closure."""
        assert sample_finding.due_date is not None

    def test_finding_lifecycle_statuses(self):
        """Finding should follow proper lifecycle."""
        valid_statuses = [
            FindingStatusEnum.OPEN,
            FindingStatusEnum.CORRECTIVE_ACTION_PENDING,
            FindingStatusEnum.VERIFICATION_PENDING,
            FindingStatusEnum.CLOSED,
        ]
        for status in valid_statuses:
            assert status in FindingStatusEnum


# ============================================================================
# Test: Corrective Actions (CAPA)
# ============================================================================

class TestCorrectiveAction:
    """Tests for CAPA endpoints."""

    def test_capa_links_to_finding(self, sample_capa, sample_finding):
        """CAPA should link to a finding."""
        assert sample_capa.finding_id == sample_finding.id

    def test_capa_has_type(self, sample_capa):
        """CAPA should have corrective or preventive type."""
        valid_types = [CAPATypeEnum.CORRECTIVE, CAPATypeEnum.PREVENTIVE]
        assert sample_capa.capa_type in valid_types

    def test_capa_has_effectiveness_criteria(self, sample_capa):
        """CAPA should define how effectiveness will be verified."""
        assert sample_capa.effectiveness_criteria is not None

    def test_capa_has_completion_date(self, sample_capa):
        """CAPA should have planned completion date."""
        assert sample_capa.planned_completion is not None


# ============================================================================
# Test: Management Review
# ============================================================================

class TestManagementReview:
    """Tests for Management Review endpoints."""

    def test_review_has_chairperson(self, sample_management_review):
        """Management review must have a chairperson (top management)."""
        assert sample_management_review.chairperson is not None

    def test_review_has_review_period(self, sample_management_review):
        """Review should cover a specific period."""
        assert sample_management_review.review_period_start is not None
        assert sample_management_review.review_period_end is not None

    def test_review_id_includes_quarter(self, sample_management_review):
        """Review ID should indicate the quarter."""
        assert "Q" in sample_management_review.review_id

    def test_review_tracks_attendees(self, sample_management_review):
        """Review should track attendees."""
        assert sample_management_review.attendees is not None
        assert len(sample_management_review.attendees) >= 1


# ============================================================================
# Test: Internal Audit
# ============================================================================

class TestInternalAudit:
    """Tests for Internal Audit endpoints."""

    def test_audit_has_scope(self, sample_internal_audit):
        """Internal audit should define scope."""
        assert sample_internal_audit.scope_description is not None

    def test_audit_covers_iso_chapters(self, sample_internal_audit):
        """Audit should specify which ISO chapters are covered."""
        assert sample_internal_audit.iso_chapters_covered is not None
        assert len(sample_internal_audit.iso_chapters_covered) >= 1

    def test_audit_has_lead_auditor(self, sample_internal_audit):
        """Audit should have a lead auditor."""
        assert sample_internal_audit.lead_auditor is not None

    def test_audit_has_criteria(self, sample_internal_audit):
        """Audit should define audit criteria."""
        assert sample_internal_audit.criteria is not None


# ============================================================================
# Test: ISMS Readiness Check
# ============================================================================

class TestISMSReadinessCheck:
    """Tests for ISMS Readiness Check."""

    def test_readiness_check_identifies_potential_majors(self):
        """Readiness check should identify potential major findings."""
        check = ISMSReadinessCheckDB(
            id=str(uuid4()),
            check_date=datetime.now(timezone.utc),
            triggered_by="admin@breakpilot.de",
            overall_status="not_ready",
            certification_possible=False,
            chapter_4_status="fail",
            chapter_5_status="fail",
            chapter_6_status="warning",
            chapter_7_status="pass",
            chapter_8_status="pass",
            chapter_9_status="fail",
            chapter_10_status="pass",
            potential_majors=[
                {"check": "ISMS Scope not approved", "iso_reference": "4.3"},
                {"check": "Master policy not approved", "iso_reference": "5.2"},
                {"check": "No internal audit conducted", "iso_reference": "9.2"},
            ],
            potential_minors=[
                {"check": "Risk treatment incomplete", "iso_reference": "6.1.2"},
            ],
            readiness_score=30.0,
        )

        assert not check.certification_possible
        assert len(check.potential_majors) >= 1
        assert check.readiness_score < 100

    def test_readiness_check_shows_chapter_status(self):
        """Readiness check should show status for each ISO chapter."""
        check = ISMSReadinessCheckDB(
            id=str(uuid4()),
            check_date=datetime.now(timezone.utc),
            triggered_by="admin@breakpilot.de",
            overall_status="ready",
            certification_possible=True,
            chapter_4_status="pass",
            chapter_5_status="pass",
            chapter_6_status="pass",
            chapter_7_status="pass",
            chapter_8_status="pass",
            chapter_9_status="pass",
            chapter_10_status="pass",
            potential_majors=[],
            potential_minors=[],
            readiness_score=100.0,
        )

        assert check.chapter_4_status == "pass"
        assert check.chapter_5_status == "pass"
        assert check.chapter_9_status == "pass"
        assert check.certification_possible


# ============================================================================
# Test: ISO 27001 Annex A Coverage
# ============================================================================

class TestAnnexACoverage:
    """Tests for ISO 27001 Annex A control coverage."""

    def test_annex_a_has_93_controls(self):
        """ISO 27001:2022 has exactly 93 controls."""
        from compliance.data.iso27001_annex_a import ISO27001_ANNEX_A_CONTROLS, ANNEX_A_SUMMARY

        assert len(ISO27001_ANNEX_A_CONTROLS) == 93
        assert ANNEX_A_SUMMARY["total_controls"] == 93

    def test_annex_a_categories(self):
        """Annex A should have 4 control categories."""
        from compliance.data.iso27001_annex_a import ANNEX_A_SUMMARY

        # A.5 Organizational (37), A.6 People (8), A.7 Physical (14), A.8 Technological (34)
        assert ANNEX_A_SUMMARY["organizational_controls"] == 37
        assert ANNEX_A_SUMMARY["people_controls"] == 8
        assert ANNEX_A_SUMMARY["physical_controls"] == 14
        assert ANNEX_A_SUMMARY["technological_controls"] == 34

    def test_annex_a_control_structure(self):
        """Each Annex A control should have required fields."""
        from compliance.data.iso27001_annex_a import ISO27001_ANNEX_A_CONTROLS

        for control in ISO27001_ANNEX_A_CONTROLS:
            assert "control_id" in control
            assert "title" in control
            assert "category" in control
            assert "description" in control
            assert control["control_id"].startswith("A.")


# ============================================================================
# Test: Audit Trail
# ============================================================================

class TestAuditTrail:
    """Tests for Audit Trail functionality."""

    def test_audit_trail_entry_has_required_fields(self):
        """Audit trail entry should have all required fields."""
        entry = AuditTrailDB(
            id=str(uuid4()),
            entity_type="isms_scope",
            entity_id=str(uuid4()),
            entity_name="ISMS Scope v1.0",
            action="approve",
            performed_by="ceo@breakpilot.de",
            performed_at=datetime.now(timezone.utc),
            checksum="sha256_hash",
        )

        assert entry.entity_type is not None
        assert entry.entity_id is not None
        assert entry.action is not None
        assert entry.performed_by is not None
        assert entry.performed_at is not None
        assert entry.checksum is not None

    def test_audit_trail_tracks_changes(self):
        """Audit trail should track field changes."""
        entry = AuditTrailDB(
            id=str(uuid4()),
            entity_type="isms_policy",
            entity_id=str(uuid4()),
            entity_name="POL-ISMS-001",
            action="update",
            field_changed="status",
            old_value="draft",
            new_value="approved",
            change_summary="Policy approved by CEO",
            performed_by="ceo@breakpilot.de",
            performed_at=datetime.now(timezone.utc),
            checksum="sha256_hash",
        )

        assert entry.field_changed == "status"
        assert entry.old_value == "draft"
        assert entry.new_value == "approved"


# ============================================================================
# Test: Certification Blockers
# ============================================================================

class TestCertificationBlockers:
    """Tests for certification blocking scenarios."""

    def test_open_major_blocks_certification(self):
        """Open major findings should block certification."""
        finding = AuditFindingDB(
            id=str(uuid4()),
            finding_id="FIND-2026-001",
            finding_type=FindingTypeEnum.MAJOR,
            title="Critical finding",
            description="Test",
            auditor="auditor@test.de",
            status=FindingStatusEnum.OPEN,
        )

        is_blocking = (finding.finding_type == FindingTypeEnum.MAJOR and
                       finding.status != FindingStatusEnum.CLOSED)
        assert is_blocking

    def test_closed_major_allows_certification(self):
        """Closed major findings should not block certification."""
        finding = AuditFindingDB(
            id=str(uuid4()),
            finding_id="FIND-2026-001",
            finding_type=FindingTypeEnum.MAJOR,
            title="Critical finding",
            description="Test",
            auditor="auditor@test.de",
            status=FindingStatusEnum.CLOSED,
            closed_date=date.today(),
        )

        is_blocking = (finding.finding_type == FindingTypeEnum.MAJOR and
                       finding.status != FindingStatusEnum.CLOSED)
        assert not is_blocking

    def test_minor_findings_dont_block_certification(self):
        """Minor findings should not block certification."""
        finding = AuditFindingDB(
            id=str(uuid4()),
            finding_id="FIND-2026-002",
            finding_type=FindingTypeEnum.MINOR,
            title="Minor finding",
            description="Test",
            auditor="auditor@test.de",
            status=FindingStatusEnum.OPEN,
        )

        is_blocking = (finding.finding_type == FindingTypeEnum.MAJOR and
                       finding.status != FindingStatusEnum.CLOSED)
        assert not is_blocking
