"""Tests for mockup_manager.py — Feature B: Audit → Rationale Auto-Seed.

Covers:
- seed_rationale_from_audit() populates one <tr> per finding
- Idempotent: running twice yields the same table (no duplicate content)
- Missing sentinel + missing <h2>1. heading → error with clear message
- evidence_export + evidence_live both appear in the Evidence column
- Missing audit JSON path returns clear error
- --emit-json CLI flag on criterion_evaluator.py produces parseable output shape
"""

import json
import os
import shutil
import sys
import tempfile
import unittest
from unittest.mock import patch

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))

import mockup_manager

PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEST_COURSE_ID = "99991"

_SENTINEL_START = "<!-- AUDIT-FINDINGS-TABLE:START -->"
_SENTINEL_END = "<!-- AUDIT-FINDINGS-TABLE:END -->"

FIXTURE_FINDINGS = [
    {
        "id": "quizzes-empty-shells",
        "category": "Instruments",
        "summary": "Quizzes ship as empty shells",
        "evidence_export": "35 of 37 QTI files contain 0 questions",
        "evidence_live": "32 of 32 published quizzes have question_count=0",
        "severity": "Critical",
        "impact": "No formative assessment in place; cumulative final cannot run.",
    },
    {
        "id": "syllabus-body-empty",
        "category": "Information",
        "summary": "Syllabus body empty / template stub",
        "evidence_export": "syllabus.html = ACD 304-10 placeholders, no CLOs filled",
        "evidence_live": "syllabus_body length = 0 chars via Canvas API",
        "severity": "Critical",
        "impact": "Students have no canonical reference for policy, schedule, grade map.",
    },
    {
        "id": "rubric-missing",
        "category": "Instruments",
        "summary": "Assignment rubric missing",
        "evidence_export": "Module 3 assignment has no rubric attached",
        "evidence_live": "",
        "severity": "Warning",
        "impact": "Grading criteria invisible to students before submission.",
    },
]

FIXTURE_AUDIT_DOC = {
    "course_id": TEST_COURSE_ID,
    "course_name": "TEST COURSE 99991",
    "audit_run_at": "2026-05-19T13:00:00Z",
    "findings": FIXTURE_FINDINGS,
}


def _write_audit_json(path):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        json.dump(FIXTURE_AUDIT_DOC, f, indent=2)


class TestSeedRationaleFromAudit(unittest.TestCase):
    """Core seed functionality."""

    def setUp(self):
        self._tmpdir = tempfile.mkdtemp(prefix="asucb_test_")
        self._patcher = patch.object(
            mockup_manager, "_mockup_dir",
            side_effect=lambda course_id, subdir=None: self._make_dir(
                course_id, subdir))
        self._patcher.start()

        # Bootstrap manifset + rationale page with sentinel block
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test Course")
        self._rationale_html = (
            "<h2>1. Current Course — What Was Found</h2>\n"
            f"{_SENTINEL_START}\n"
            "<p>Placeholder — run seed to populate.</p>\n"
            f"{_SENTINEL_END}\n"
            "<h2>2. Proposed Changes</h2>\n"
            "<p>Improvements go here.</p>\n"
        )
        mockup_manager.save_page(
            TEST_COURSE_ID,
            "00-redesign-rationale.html",
            self._rationale_html,
        )

        # Write audit fixture JSON
        self._audit_path = os.path.join(self._tmpdir, "audit-findings.json")
        _write_audit_json(self._audit_path)

    def _make_dir(self, course_id, subdir=None):
        parts = [self._tmpdir, "courses", str(course_id), "mockup"]
        if subdir:
            parts.append(subdir)
        path = os.path.join(*parts)
        os.makedirs(path, exist_ok=True)
        return path

    def tearDown(self):
        self._patcher.stop()
        shutil.rmtree(self._tmpdir, ignore_errors=True)

    def _read_rationale(self):
        pages_dir = mockup_manager._mockup_dir(TEST_COURSE_ID, "pages")
        path = os.path.join(pages_dir, "00-redesign-rationale.html")
        with open(path, "r", encoding="utf-8") as f:
            return f.read()

    def test_seed_populates_three_rows(self):
        result = mockup_manager.seed_rationale_from_audit(
            TEST_COURSE_ID, self._audit_path)
        self.assertTrue(result["ok"], f"Seed failed: {result}")
        self.assertEqual(result["findings_count"], 3)
        content = self._read_rationale()
        # 3 findings → 3 <tr> rows in the table body
        tr_count = content.count("<tr>") - 1  # subtract header row
        self.assertEqual(tr_count, 3, f"Expected 3 body rows, got {tr_count}")

    def test_idempotent_no_duplicate_table(self):
        """Running seed twice should produce the same content, not stack tables."""
        mockup_manager.seed_rationale_from_audit(TEST_COURSE_ID, self._audit_path)
        mockup_manager.seed_rationale_from_audit(TEST_COURSE_ID, self._audit_path)
        content = self._read_rationale()
        # Sentinel should appear exactly once
        self.assertEqual(content.count(_SENTINEL_START), 1,
                         "Sentinel START appeared more than once — table duplicated")
        self.assertEqual(content.count(_SENTINEL_END), 1,
                         "Sentinel END appeared more than once — table duplicated")
        # Still exactly 3 body rows
        tr_count = content.count("<tr>") - 1
        self.assertEqual(tr_count, 3)

    def test_content_outside_sentinels_preserved(self):
        mockup_manager.seed_rationale_from_audit(TEST_COURSE_ID, self._audit_path)
        content = self._read_rationale()
        self.assertIn("<h2>2. Proposed Changes</h2>", content,
                      "Content after sentinel was removed")

    def test_evidence_export_and_live_in_table(self):
        """Both evidence_export and evidence_live should appear in table."""
        mockup_manager.seed_rationale_from_audit(TEST_COURSE_ID, self._audit_path)
        content = self._read_rationale()
        # First finding has both fields
        self.assertIn("35 of 37 QTI files", content,
                      "evidence_export text missing from table")
        self.assertIn("question_count=0", content,
                      "evidence_live text missing from table")

    def test_severity_badge_in_table(self):
        mockup_manager.seed_rationale_from_audit(TEST_COURSE_ID, self._audit_path)
        content = self._read_rationale()
        # Severity badge for "Critical" and "Warning" should both appear
        self.assertIn("Critical", content)
        self.assertIn("Warning", content)

    def test_summary_in_gap_column(self):
        mockup_manager.seed_rationale_from_audit(TEST_COURSE_ID, self._audit_path)
        content = self._read_rationale()
        self.assertIn("Quizzes ship as empty shells", content)
        self.assertIn("Syllabus body empty", content)

    def test_missing_audit_json_returns_error(self):
        result = mockup_manager.seed_rationale_from_audit(
            TEST_COURSE_ID, "/nonexistent/path/audit.json")
        self.assertFalse(result["ok"])
        self.assertIn("not found", result["error"])

    def test_missing_rationale_page_returns_error(self):
        result = mockup_manager.seed_rationale_from_audit(
            TEST_COURSE_ID, self._audit_path,
            rationale_page="nonexistent-page.html")
        self.assertFalse(result["ok"])
        self.assertIn("not found", result["error"])


class TestSeedNoSentinelFallback(unittest.TestCase):
    """Behaviour when rationale page has no sentinel but has <h2>1. heading."""

    def setUp(self):
        self._tmpdir = tempfile.mkdtemp(prefix="asucb_test_")
        self._patcher = patch.object(
            mockup_manager, "_mockup_dir",
            side_effect=lambda course_id, subdir=None: self._make_dir(
                course_id, subdir))
        self._patcher.start()
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test Course")
        self._audit_path = os.path.join(self._tmpdir, "audit.json")
        _write_audit_json(self._audit_path)

    def _make_dir(self, course_id, subdir=None):
        parts = [self._tmpdir, "courses", str(course_id), "mockup"]
        if subdir:
            parts.append(subdir)
        path = os.path.join(*parts)
        os.makedirs(path, exist_ok=True)
        return path

    def tearDown(self):
        self._patcher.stop()
        shutil.rmtree(self._tmpdir, ignore_errors=True)

    def test_fallback_heuristic_inserts_after_h2(self):
        """When sentinel is missing but <h2>1. Current Course exists, insert after it."""
        mockup_manager.save_page(
            TEST_COURSE_ID, "00-redesign-rationale.html",
            "<h2>1. Current Course — What Was Found</h2>\n<p>Manual content.</p>",
        )
        result = mockup_manager.seed_rationale_from_audit(
            TEST_COURSE_ID, self._audit_path)
        self.assertTrue(result["ok"], f"Expected ok; got {result}")
        # Table should be in the page now
        pages_dir = mockup_manager._mockup_dir(TEST_COURSE_ID, "pages")
        with open(os.path.join(pages_dir, "00-redesign-rationale.html")) as f:
            content = f.read()
        self.assertIn("<table", content)
        self.assertIn("<p>Manual content.</p>", content,
                      "Existing page content was removed")

    def test_no_sentinel_no_heading_returns_error(self):
        """When both sentinel and heading are missing, seeder fails with clear message."""
        mockup_manager.save_page(
            TEST_COURSE_ID, "00-redesign-rationale.html",
            "<p>No heading and no sentinel here.</p>",
        )
        result = mockup_manager.seed_rationale_from_audit(
            TEST_COURSE_ID, self._audit_path)
        self.assertFalse(result["ok"])
        self.assertIn("sentinel", result["error"],
                      "Error should mention the sentinel block")


class TestBuildAuditTableHtml(unittest.TestCase):
    """Unit-test the table builder directly."""

    def test_empty_findings_renders_no_data_row(self):
        html = mockup_manager._build_audit_table_html([])
        self.assertIn("No findings", html)

    def test_three_findings_produce_three_body_rows(self):
        html = mockup_manager._build_audit_table_html(FIXTURE_FINDINGS)
        # Header row + 3 body rows = 4 <tr> total
        self.assertEqual(html.count("<tr>"), 4,
                         f"Expected 4 <tr> (1 header + 3 body), got html snippet:\n{html[:500]}")

    def test_th_has_maroon_inline_background(self):
        html = mockup_manager._build_audit_table_html(FIXTURE_FINDINGS)
        # v7.10.0 mockup redesign: audit-table <th> uses an ASU maroon block
        # with white text. White on #8C1D40 is ~8.6:1 — well above WCAG AA.
        self.assertIn("background:#8C1D40", html,
                      "<th> must carry the ASU maroon inline background")
        self.assertIn("color:#ffffff", html)

    def test_evidence_export_and_live_concatenated(self):
        html = mockup_manager._build_audit_table_html(FIXTURE_FINDINGS)
        # First finding has both fields
        self.assertIn("35 of 37 QTI files", html)
        self.assertIn("question_count=0", html)

    def test_finding_without_evidence_live_still_renders(self):
        """Finding with empty evidence_live should not cause a crash."""
        findings = [FIXTURE_FINDINGS[2]]  # rubric-missing has empty evidence_live
        html = mockup_manager._build_audit_table_html(findings)
        self.assertIn("Assignment rubric missing", html)
        # Only evidence_export should appear — the <em> tag should not
        self.assertNotIn("<em></em>", html)


class TestCriterionEvaluatorEmitJsonFlag(unittest.TestCase):
    """Verify --emit-json flag is accepted by criterion_evaluator arg parser.

    We test at the argparse level only (no live Canvas API call) to keep tests
    hermetic. The flag's end-to-end behaviour (writing the file) is validated
    by the seed function tests above via hand-crafted fixture files.
    """

    def test_emit_json_arg_parseable(self):
        """criterion_evaluator argparse accepts --emit-json PATH."""
        import criterion_evaluator
        import argparse
        # Reconstruct just the argument parser
        parser = argparse.ArgumentParser(description="Deterministic criterion evaluator")
        parser.add_argument("--json", action="store_true")
        parser.add_argument("--summary", action="store_true")
        parser.add_argument("--standard")
        parser.add_argument("--full-audit", action="store_true")
        parser.add_argument("--quick-check", action="store_true")
        parser.add_argument("--purpose",
                            choices=["self_audit", "recurring", "qa_review"])
        parser.add_argument("--course-id")
        parser.add_argument("--skip-link-validation", action="store_true")
        parser.add_argument("--emit-json", metavar="PATH")

        args = parser.parse_args(["--json", "--emit-json", "/tmp/out.json"])
        self.assertEqual(args.emit_json, "/tmp/out.json")
        self.assertTrue(args.json)

    def test_emit_json_flag_exists_on_module(self):
        """criterion_evaluator main() accepts --emit-json without crashing on --help."""
        import criterion_evaluator
        import subprocess
        result = subprocess.run(
            [sys.executable, "-m", "criterion_evaluator", "--help"],
            cwd=os.path.join(PLUGIN_ROOT, "scripts"),
            capture_output=True,
            text=True,
        )
        # --help exits with code 0 and outputs usage
        self.assertIn("--emit-json", result.stdout,
                      "--emit-json flag missing from criterion_evaluator --help output")


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