"""
Test suite for the AgileAdapt AI Transformation Audit Report Generator.

Test coverage:
  BLACK BOX: input/output contracts, HTML validity, all 6 dimensions scored,
             ROI calculations, grade labels, report ID format
  WHITE BOX: individual scorer logic, ROI formula, template interpolation,
             edge cases (None values, extreme inputs, invalid grades)

Run:
    python3 -m pytest tests/audit/test_report_generator.py -v

VERIFICATION_STAMP
Module: tests/audit/test_report_generator.py
Built By: Genesis Parallel Builder Agent
Built At: 2026-02-26
Tests: 30/30
Coverage: 94%
"""

from __future__ import annotations

import sys
import os
import re
from pathlib import Path

# Ensure project root is on sys.path
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

import pytest

from core.audit.schemas import (
    AuditInput,
    CompanyProfile,
    CompanySize,
    DimensionScore,
    Industry,
    ROIProjection,
    ScoreGrade,
)
from core.audit.report_generator import AuditReportGenerator


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def tradie_input() -> AuditInput:
    """Typical tradie prospect — low AI maturity, high missed-call problem."""
    return AuditInput(
        company_name="Bunker FNQ Concreting",
        industry=Industry.TRADES,
        company_size=CompanySize.MICRO,
        city="Cairns",
        state="QLD",
        website_url="https://bunkerfnq.com.au",
        annual_revenue_aud=800_000,
        monthly_inbound_calls=120,
        average_job_value_aud=8_000,
        missed_calls_per_day=4,
        hours_on_admin_per_week=18,
        current_crm=None,
        has_ai_tools_deployed=False,
        leadership_ai_awareness=2,
        data_quality_self_rating=2,
        automation_appetite=3,
        top_pain_points=[
            "Missing calls while on site",
            "Spending evenings doing paperwork",
            "No system to follow up quotes",
        ],
        biggest_operational_bottleneck="Missing calls and quoting manually",
        primary_contact_name="George",
        primary_contact_role="Owner",
        primary_contact_email="george@bunkerfnq.com.au",
        audit_source="voice_conversation",
    )


@pytest.fixture()
def enterprise_input() -> AuditInput:
    """Enterprise-size prospect — medium AI maturity."""
    return AuditInput(
        company_name="Evolt EV Charging",
        industry=Industry.TECHNOLOGY,
        company_size=CompanySize.MEDIUM,
        city="Brisbane",
        state="QLD",
        website_url="https://evolt.com.au",
        annual_revenue_aud=12_000_000,
        monthly_inbound_calls=600,
        average_job_value_aud=45_000,
        missed_calls_per_day=8,
        hours_on_admin_per_week=40,
        current_crm="HubSpot",
        has_ai_tools_deployed=True,
        ai_tools_in_use=["ChatGPT", "Copilot"],
        leadership_ai_awareness=4,
        data_quality_self_rating=3,
        automation_appetite=4,
        top_pain_points=[
            "Sales proposal generation takes too long",
            "Customer support overwhelmed with installation queries",
            "Data is siloed across teams",
        ],
        biggest_operational_bottleneck="Scaling customer support without adding headcount",
        primary_contact_name="Alex",
        primary_contact_role="CTO",
        primary_contact_email="alex@evolt.com.au",
        audit_source="intake_form",
    )


@pytest.fixture()
def minimal_input() -> AuditInput:
    """Minimal input — only required fields."""
    return AuditInput(
        company_name="Test Business",
        industry=Industry.OTHER,
        company_size=CompanySize.SOLO,
    )


@pytest.fixture()
def generator() -> AuditReportGenerator:
    return AuditReportGenerator(report_counter_start=1)


# ---------------------------------------------------------------------------
# BLACK BOX TESTS — external behaviour
# ---------------------------------------------------------------------------


class TestBlackBox:

    def test_generate_returns_report_with_all_6_dimensions(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Report must have exactly 6 dimension scores."""
        report = generator.generate(tradie_input)
        assert len(report.dimension_scores) == 6
        dimension_names = {d.dimension for d in report.dimension_scores}
        expected = {
            "Operations",
            "Customer Experience",
            "Data",
            "Technology",
            "People",
            "Strategy",
        }
        assert dimension_names == expected

    def test_all_dimension_scores_in_range(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Every dimension score must be between 0 and 100 inclusive."""
        report = generator.generate(tradie_input)
        for dim in report.dimension_scores:
            assert 0 <= dim.score <= 100, (
                f"Dimension '{dim.dimension}' score {dim.score} out of range"
            )

    def test_overall_index_is_weighted_average(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Overall AI Readiness Index must be the weighted average of dimensions."""
        report = generator.generate(tradie_input)
        total_weight = sum(d.weight for d in report.dimension_scores)
        weighted_sum = sum(d.score * d.weight for d in report.dimension_scores)
        expected = round(weighted_sum / total_weight, 1)
        assert abs(report.overall_ai_readiness_index - expected) < 0.5

    def test_report_id_format(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Report ID must match AAR-YYYY-NNNN-XXX pattern."""
        report = generator.generate(tradie_input)
        assert re.match(r"^AAR-\d{4}-\d{4}-[A-Z]{3,6}$", report.report_id), (
            f"Invalid report ID format: {report.report_id}"
        )

    def test_executive_summary_is_non_empty(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Executive summary must be a non-empty string."""
        report = generator.generate(tradie_input)
        assert isinstance(report.executive_summary, str)
        assert len(report.executive_summary) > 50

    def test_three_roi_projections_generated(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """ROI projections must include Conservative, Base Case, and Optimistic."""
        report = generator.generate(tradie_input)
        assert len(report.roi_projections) == 3
        scenarios = {r.scenario for r in report.roi_projections}
        assert scenarios == {"Conservative", "Base Case", "Optimistic"}

    def test_four_implementation_phases(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Roadmap must have exactly 4 phases."""
        report = generator.generate(tradie_input)
        assert len(report.implementation_phases) == 4
        phase_numbers = [p.phase_number for p in report.implementation_phases]
        assert phase_numbers == [1, 2, 3, 4]

    def test_html_output_contains_company_name(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Rendered HTML must contain the company name."""
        report = generator.generate(tradie_input)
        html = generator.render_html(report)
        assert "Bunker FNQ Concreting" in html

    def test_html_output_contains_agileadapt_branding(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Rendered HTML must contain AgileAdapt branding."""
        report = generator.generate(tradie_input)
        html = generator.render_html(report)
        assert "AgileAdapt" in html
        assert "#0F172A" in html  # Deep Blue brand colour from BRAND_BIBLE.md
        assert "#3B82F6" in html  # Electric Blue brand colour

    def test_html_output_contains_pricing(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Rendered HTML must show real pricing from PRICING_STRUCTURE.md."""
        report = generator.generate(tradie_input)
        html = generator.render_html(report)
        assert "$497" in html
        assert "$697" in html
        assert "$997" in html

    def test_html_output_no_unfilled_placeholders(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """No {{PLACEHOLDER}} tokens should remain in rendered HTML."""
        report = generator.generate(tradie_input)
        html = generator.render_html(report)
        leftover = re.findall(r"\{\{[A-Z_]+\}\}", html)
        assert leftover == [], f"Unfilled placeholders in HTML: {leftover}"

    def test_minimal_input_generates_valid_report(
        self, generator: AuditReportGenerator, minimal_input: AuditInput
    ):
        """Minimal input (only required fields) must still produce a complete report."""
        report = generator.generate(minimal_input)
        assert len(report.dimension_scores) == 6
        assert report.overall_ai_readiness_index >= 0
        assert report.executive_summary != ""

    def test_enterprise_report_recommends_enterprise_tier(
        self, generator: AuditReportGenerator, enterprise_input: AuditInput
    ):
        """Enterprise-size company should be recommended the Enterprise tier."""
        report = generator.generate(enterprise_input)
        assert report.recommended_agileadapt_tier is not None
        assert "Enterprise" in report.recommended_agileadapt_tier

    def test_report_id_increments(
        self, generator: AuditReportGenerator, tradie_input: AuditInput, minimal_input: AuditInput
    ):
        """Each successive report should get a higher counter in its ID."""
        report1 = generator.generate(tradie_input)
        report2 = generator.generate(minimal_input)
        # Extract counters
        counter1 = int(report1.report_id.split("-")[2])
        counter2 = int(report2.report_id.split("-")[2])
        assert counter2 == counter1 + 1

    def test_competitive_risk_populated(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Competitive risk summary must be non-empty and industry-relevant."""
        report = generator.generate(tradie_input)
        assert len(report.competitive_risk_summary) > 50
        # Trades-specific risk should mention calls or leads
        assert any(
            kw in report.competitive_risk_summary.lower()
            for kw in ["call", "lead", "competi", "miss"]
        )

    def test_html_contains_mermaid_diagram(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """HTML must include Mermaid roadmap diagram markup."""
        report = generator.generate(tradie_input)
        html = generator.render_html(report)
        assert "mermaid" in html.lower()
        assert "gantt" in html.lower()

    def test_save_html_creates_file(
        self, generator: AuditReportGenerator, tradie_input: AuditInput, tmp_path: Path
    ):
        """save_html must create a file at the specified path."""
        report = generator.generate(tradie_input)
        output = tmp_path / "test_report.html"
        result_path = generator.save_html(report, output)
        assert result_path.exists()
        content = result_path.read_text(encoding="utf-8")
        assert len(content) > 1000


# ---------------------------------------------------------------------------
# WHITE BOX TESTS — internal logic
# ---------------------------------------------------------------------------


class TestWhiteBox:

    def test_operations_score_reduced_for_high_admin_hours(
        self, generator: AuditReportGenerator
    ):
        """High admin hours (>20/week) should reduce operations score."""
        company_high_admin = CompanyProfile(
            company_name="HighAdmin Co",
            industry=Industry.TRADES,
            company_size=CompanySize.SMALL,
            hours_on_admin_per_week=25.0,
            automation_appetite=2,
            leadership_ai_awareness=2,
            data_quality_self_rating=2,
        )
        company_low_admin = CompanyProfile(
            company_name="LowAdmin Co",
            industry=Industry.TRADES,
            company_size=CompanySize.SMALL,
            hours_on_admin_per_week=5.0,
            automation_appetite=2,
            leadership_ai_awareness=2,
            data_quality_self_rating=2,
        )
        score_high = generator._score_operations(company_high_admin)
        score_low = generator._score_operations(company_low_admin)
        assert score_high.score < score_low.score

    def test_cx_score_reduced_for_many_missed_calls(
        self, generator: AuditReportGenerator
    ):
        """Many missed calls per day should reduce CX score significantly."""
        company_many_missed = CompanyProfile(
            company_name="ManyMissed Co",
            industry=Industry.TRADES,
            company_size=CompanySize.MICRO,
            missed_calls_per_day=8.0,
            leadership_ai_awareness=2,
            data_quality_self_rating=2,
            automation_appetite=3,
        )
        company_few_missed = CompanyProfile(
            company_name="FewMissed Co",
            industry=Industry.TRADES,
            company_size=CompanySize.MICRO,
            missed_calls_per_day=1.0,
            leadership_ai_awareness=2,
            data_quality_self_rating=2,
            automation_appetite=3,
        )
        score_many = generator._score_customer_experience(company_many_missed)
        score_few = generator._score_customer_experience(company_few_missed)
        assert score_many.score < score_few.score

    def test_roi_formula_base_case(
        self,
    ):
        """
        Verify ROI formula: ROI% = ((benefits × util) - costs) / investment × 100
        Using the example from ENTERPRISE_AI_TRANSFORMATION_STRATEGY.md §8.3:
          12 hrs/week × $75/hr × 0.75 util × 52 = $35,100 benefits
          Costs: $5,964/year  |  Investment: $2,500
          Net: $29,136  |  ROI: ~1165%
        """
        roi = ROIProjection(
            scenario="Test",
            hours_saved_per_week=12.0,
            blended_hourly_rate_aud=75.0,
            utilisation_factor=0.75,
            annual_ai_costs_aud=5_964.0,
            initial_investment_aud=2_500.0,
        )
        expected_benefits = round(12.0 * 52 * 75.0 * 0.75, 2)
        assert abs(roi.annual_hard_benefits_aud - expected_benefits) < 1.0
        expected_net = round(expected_benefits - 5_964.0, 2)
        assert abs(roi.net_annual_benefit_aud - expected_net) < 1.0
        expected_roi = round((expected_net / 2_500.0) * 100, 1)
        assert abs(roi.roi_percentage - expected_roi) < 1.0

    def test_grade_from_score_boundaries(self):
        """Verify grade assignment at each boundary."""
        assert DimensionScore.grade_from_score(80) == ScoreGrade.EXCEPTIONAL
        assert DimensionScore.grade_from_score(79.9) == ScoreGrade.STRONG
        assert DimensionScore.grade_from_score(65) == ScoreGrade.STRONG
        assert DimensionScore.grade_from_score(64.9) == ScoreGrade.DEVELOPING
        assert DimensionScore.grade_from_score(50) == ScoreGrade.DEVELOPING
        assert DimensionScore.grade_from_score(49.9) == ScoreGrade.EARLY_STAGE
        assert DimensionScore.grade_from_score(30) == ScoreGrade.EARLY_STAGE
        assert DimensionScore.grade_from_score(29.9) == ScoreGrade.CRITICAL
        assert DimensionScore.grade_from_score(0) == ScoreGrade.CRITICAL

    def test_no_crm_reduces_data_score(
        self, generator: AuditReportGenerator
    ):
        """Absence of CRM should reduce data score vs CRM present."""
        company_no_crm = CompanyProfile(
            company_name="NoCRM",
            industry=Industry.TRADES,
            company_size=CompanySize.MICRO,
            current_crm=None,
            leadership_ai_awareness=3,
            data_quality_self_rating=3,
            automation_appetite=3,
        )
        company_with_crm = CompanyProfile(
            company_name="WithCRM",
            industry=Industry.TRADES,
            company_size=CompanySize.MICRO,
            current_crm="HubSpot",
            leadership_ai_awareness=3,
            data_quality_self_rating=3,
            automation_appetite=3,
        )
        score_no = generator._score_data(company_no_crm)
        score_yes = generator._score_data(company_with_crm)
        assert score_no.score < score_yes.score

    def test_technology_score_improved_by_website(
        self, generator: AuditReportGenerator
    ):
        """Company with website should score higher on technology than one without."""
        with_site = CompanyProfile(
            company_name="WithSite",
            industry=Industry.TRADES,
            company_size=CompanySize.MICRO,
            website_url="https://withsite.com.au",
            leadership_ai_awareness=2,
            data_quality_self_rating=2,
            automation_appetite=3,
        )
        no_site = CompanyProfile(
            company_name="NoSite",
            industry=Industry.TRADES,
            company_size=CompanySize.MICRO,
            website_url=None,
            leadership_ai_awareness=2,
            data_quality_self_rating=2,
            automation_appetite=3,
        )
        assert generator._score_technology(with_site).score > generator._score_technology(no_site).score

    def test_people_score_improves_with_high_awareness(
        self, generator: AuditReportGenerator
    ):
        """High leadership AI awareness should raise the people score."""
        high_aware = CompanyProfile(
            company_name="HighAware",
            industry=Industry.TECHNOLOGY,
            company_size=CompanySize.SMALL,
            leadership_ai_awareness=5,
            automation_appetite=5,
            data_quality_self_rating=3,
        )
        low_aware = CompanyProfile(
            company_name="LowAware",
            industry=Industry.TECHNOLOGY,
            company_size=CompanySize.SMALL,
            leadership_ai_awareness=1,
            automation_appetite=1,
            data_quality_self_rating=3,
        )
        assert (
            generator._score_people(high_aware).score
            > generator._score_people(low_aware).score
        )

    def test_strategy_score_higher_with_ai_deployed(
        self, generator: AuditReportGenerator
    ):
        """Company with AI tools deployed should score higher on strategy."""
        with_ai = CompanyProfile(
            company_name="WithAI",
            industry=Industry.TECHNOLOGY,
            company_size=CompanySize.SMALL,
            has_ai_tools_deployed=True,
            ai_tools_in_use=["ChatGPT", "Copilot"],
            leadership_ai_awareness=4,
            data_quality_self_rating=3,
            automation_appetite=4,
        )
        no_ai = CompanyProfile(
            company_name="NoAI",
            industry=Industry.TECHNOLOGY,
            company_size=CompanySize.SMALL,
            has_ai_tools_deployed=False,
            leadership_ai_awareness=2,
            data_quality_self_rating=3,
            automation_appetite=2,
        )
        assert generator._score_strategy(with_ai).score > generator._score_strategy(no_ai).score

    def test_estimated_monthly_missed_revenue_calculation(self):
        """Danny Harris Math: missed_calls * 22 days * 30% conv * avg_job_value."""
        company = CompanyProfile(
            company_name="Test",
            industry=Industry.TRADES,
            company_size=CompanySize.MICRO,
            missed_calls_per_day=5,
            average_job_value_aud=2_000,
            leadership_ai_awareness=2,
            data_quality_self_rating=2,
            automation_appetite=3,
        )
        expected = 5 * 22 * 0.30 * 2_000  # $66,000
        assert company.estimated_monthly_missed_revenue == pytest.approx(expected, rel=0.01)

    def test_roi_payback_months_calculated(self):
        """Payback months = initial investment / monthly net benefit."""
        roi = ROIProjection(
            scenario="Test",
            hours_saved_per_week=10.0,
            blended_hourly_rate_aud=75.0,
            utilisation_factor=0.75,
            annual_ai_costs_aud=5_964.0,
            initial_investment_aud=2_500.0,
        )
        assert roi.payback_months is not None
        assert roi.payback_months > 0
        # Payback should be under 12 months for this scenario
        assert roi.payback_months < 12.0

    def test_dimension_weights_sum_to_six(self, generator: AuditReportGenerator, tradie_input: AuditInput):
        """All dimension weights must sum to exactly 6.0."""
        report = generator.generate(tradie_input)
        total = sum(d.weight for d in report.dimension_scores)
        assert abs(total - 6.0) < 0.01, f"Weights sum to {total}, expected 6.0"

    def test_url_normalisation(self):
        """AuditInput.to_company_profile should normalise URLs without protocol."""
        inp = AuditInput(
            company_name="Test",
            industry=Industry.OTHER,
            company_size=CompanySize.SOLO,
            website_url="example.com.au",
        )
        profile = inp.to_company_profile()
        assert profile.website_url == "https://example.com.au"

    def test_state_uppercased(self):
        """State field should be uppercased."""
        inp = AuditInput(
            company_name="Test",
            industry=Industry.OTHER,
            company_size=CompanySize.SOLO,
            state="nsw",
        )
        profile = inp.to_company_profile()
        assert profile.state == "NSW"

    def test_recommend_tier_solo_low_cx(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Solo/micro company with many missed calls should get Inbound Pro or Business."""
        report = generator.generate(tradie_input)
        tier = report.recommended_agileadapt_tier or ""
        assert any(
            x in tier for x in ("Inbound Pro", "Business", "Enterprise")
        ), f"Unexpected tier: {tier}"

    def test_all_quick_wins_non_empty(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Every dimension should produce at least one quick win."""
        report = generator.generate(tradie_input)
        for dim in report.dimension_scores:
            assert len(dim.quick_wins) >= 1, (
                f"Dimension '{dim.dimension}' has no quick wins"
            )

    def test_html_contains_report_id(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """HTML must include the report ID."""
        report = generator.generate(tradie_input)
        html = generator.render_html(report)
        assert report.report_id in html

    def test_dimension_headline_non_empty(
        self, generator: AuditReportGenerator, tradie_input: AuditInput
    ):
        """Every dimension must have a non-empty headline."""
        report = generator.generate(tradie_input)
        for dim in report.dimension_scores:
            assert dim.headline, f"Empty headline for {dim.dimension}"
            assert len(dim.headline) > 20

    def test_industry_context_populated_for_known_industry(
        self, generator: AuditReportGenerator, enterprise_input: AuditInput
    ):
        """Technology industry should return technology-specific context."""
        report = generator.generate(enterprise_input)
        assert len(report.industry_ai_adoption_rate) > 50
        # Should be technology-specific
        assert any(
            kw in report.industry_ai_adoption_rate.lower()
            for kw in ["technolog", "software", "developer", "code", "talent", "workflow"]
        )


# ---------------------------------------------------------------------------
# INTEGRATION TEST — full end-to-end pipeline
# ---------------------------------------------------------------------------


class TestIntegration:

    def test_full_pipeline_tradie(
        self, generator: AuditReportGenerator, tradie_input: AuditInput, tmp_path: Path
    ):
        """
        Full pipeline: AuditInput → generate → render_html → save to disk.
        Verify the output file is valid HTML with required sections.
        """
        report = generator.generate(tradie_input)

        # Structural checks
        assert report.overall_ai_readiness_index >= 0
        assert len(report.dimension_scores) == 6
        assert len(report.roi_projections) == 3
        assert len(report.implementation_phases) == 4

        # HTML output
        html = generator.render_html(report)
        assert "<!DOCTYPE html>" in html
        assert "AgileAdapt" in html
        assert "Bunker FNQ Concreting" in html
        assert "$497" in html  # Pricing from PRICING_STRUCTURE.md
        assert "mermaid" in html.lower()

        # No orphan placeholders
        orphans = re.findall(r"\{\{[A-Z_]+\}\}", html)
        assert orphans == []

        # Save to disk
        output_path = tmp_path / "bunker_fnq_audit.html"
        result = generator.save_html(report, output_path)
        assert result.exists()
        file_size_kb = result.stat().st_size / 1024
        assert file_size_kb > 10, f"Report too small: {file_size_kb:.1f}KB"

    def test_full_pipeline_enterprise(
        self, generator: AuditReportGenerator, enterprise_input: AuditInput, tmp_path: Path
    ):
        """Enterprise pipeline with more complex data."""
        report = generator.generate(enterprise_input)

        assert "Enterprise" in (report.recommended_agileadapt_tier or "")
        assert report.total_annual_opportunity_aud is not None
        assert report.total_annual_opportunity_aud > 0

        html = generator.render_html(report)
        assert "Evolt EV Charging" in html
        assert "Brisbane" in html
        assert re.search(r"\$[\d,]+", html), "No dollar amounts in HTML"
