"""Tests for scripts/check_asu_brand.py — ASU brand compliance linter."""

import json
import subprocess
import sys
from pathlib import Path

import pytest

# Import module under test
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
import check_asu_brand as brand

FIXTURES = Path(__file__).parent / "fixtures" / "brand"
CLEAN_HTML = FIXTURES / "clean.html"
OFFBRAND_HTML = FIXTURES / "offbrand.html"

# ── Helpers ───────────────────────────────────────────────────────────────────

def _run_on(path):
    """Run check_file() on a single Path and return (failures, warnings)."""
    return brand.check_file(path)


def _run_report(path):
    """Run run() and return the report dict."""
    return brand.run([str(path)])


# ── Palette constant ──────────────────────────────────────────────────────────

class TestPalette:
    def test_primary_colors_present(self):
        assert "#8c1d40" in brand.ASU_PALETTE
        assert "#ffc627" in brand.ASU_PALETTE
        assert "#000000" in brand.ASU_PALETTE
        assert "#ffffff" in brand.ASU_PALETTE

    def test_secondary_colors_present(self):
        assert "#78be20" in brand.ASU_PALETTE
        assert "#00a3e0" in brand.ASU_PALETTE
        assert "#ff7f32" in brand.ASU_PALETTE

    def test_shades_present(self):
        assert "#5c0f2d" in brand.ASU_PALETTE
        assert "#c99700" in brand.ASU_PALETTE


# ── Color tokenizer ───────────────────────────────────────────────────────────

class TestExtractColors:
    def test_hex6(self):
        assert "#8c1d40" in brand.extract_colors("#8C1D40", {})

    def test_hex3(self):
        colors = brand.extract_colors("#fff", {})
        assert "#ffffff" in colors

    def test_rgb(self):
        colors = brand.extract_colors("rgb(140, 29, 64)", {})
        assert "#8c1d40" in colors

    def test_rgba(self):
        colors = brand.extract_colors("rgba(140, 29, 64, 0.5)", {})
        assert "#8c1d40" in colors

    def test_named_white(self):
        assert "#ffffff" in brand.extract_colors("white", {})

    def test_named_black(self):
        assert "#000000" in brand.extract_colors("black", {})

    def test_var_resolved(self):
        root_vars = {"--asu-maroon": "#8C1D40"}
        colors = brand.extract_colors("var(--asu-maroon)", root_vars)
        assert "#8c1d40" in colors

    def test_var_unresolved_no_crash(self):
        colors = brand.extract_colors("var(--unknown-color)", {})
        assert colors == []  # unresolved var, no fallback -> no color

    def test_linear_gradient(self):
        colors = brand.extract_colors(
            "linear-gradient(135deg, #8C1D40, #FFC627)", {}
        )
        assert "#8c1d40" in colors
        assert "#ffc627" in colors

    def test_transparent_skipped(self):
        colors = brand.extract_colors("transparent", {})
        assert not colors


# ── Root vars extractor ───────────────────────────────────────────────────────

class TestExtractRootVars:
    def test_basic_extraction(self):
        css = ":root { --asu-maroon: #8C1D40; --asu-gold: #FFC627; }"
        result = brand.extract_root_vars(css)
        assert result["--asu-maroon"] == "#8C1D40"
        assert result["--asu-gold"] == "#FFC627"

    def test_multiline(self):
        css = ":root {\n    --text-dark: #333333;\n    --bg: white;\n}"
        result = brand.extract_root_vars(css)
        assert "--text-dark" in result
        assert "--bg" in result

    def test_no_root_block(self):
        css = "body { color: red; }"
        assert brand.extract_root_vars(css) == {}


# ── Font check ────────────────────────────────────────────────────────────────

class TestFontCheck:
    def _parse(self, html_fragment, fname="test.html"):
        p = brand.BrandParser(Path(fname))
        p.feed(f"<html><head></head><body>{html_fragment}</body></html>")
        p.close()
        p.finalize()
        return p.failures, p.warnings

    def test_arial_lead_passes(self):
        f, _ = self._parse(
            "<p style=\"font-family: Arial, Helvetica, sans-serif;\">ok</p>"
        )
        font_failures = [x for x in f if x["code"] == "BRAND.FONT"]
        assert not font_failures

    def test_roboto_lead_fails(self):
        f, _ = self._parse(
            "<p style=\"font-family: Roboto, Arial, sans-serif;\">bad</p>"
        )
        font_failures = [x for x in f if x["code"] == "BRAND.FONT"]
        assert font_failures

    def test_inherit_exempt(self):
        f, _ = self._parse(
            "<p style=\"font-family: inherit;\">ok</p>"
        )
        font_failures = [x for x in f if x["code"] == "BRAND.FONT"]
        assert not font_failures

    def test_monospace_exempt(self):
        f, _ = self._parse(
            "<code style=\"font-family: 'Courier New', Courier, monospace;\">code</code>"
        )
        font_failures = [x for x in f if x["code"] == "BRAND.FONT"]
        assert not font_failures

    def test_style_block_roboto_fails(self):
        html = "<html><head><style>body { font-family: Roboto, Arial, sans-serif; }</style></head><body></body></html>"
        p = brand.BrandParser(Path("test.html"))
        p.feed(html)
        p.close()
        p.finalize()
        font_failures = [x for x in p.failures if x["code"] == "BRAND.FONT"]
        assert font_failures


# ── Italic check ──────────────────────────────────────────────────────────────

class TestItalicCheck:
    def _parse_frag(self, frag):
        p = brand.BrandParser(Path("test.html"))
        p.feed(f"<html><body>{frag}</body></html>")
        p.close()
        p.finalize()
        return p.failures

    def test_i_tag_fails(self):
        failures = self._parse_frag("<p>Some <i>italic</i> text</p>")
        assert any(f["code"] == "BRAND.ITALIC" for f in failures)

    def test_em_tag_fails(self):
        failures = self._parse_frag("<p>Some <em>emphasis</em></p>")
        assert any(f["code"] == "BRAND.ITALIC" for f in failures)

    def test_font_style_italic_inline_fails(self):
        failures = self._parse_frag(
            "<p style=\"font-style: italic;\">bad</p>"
        )
        assert any(f["code"] == "BRAND.ITALIC" for f in failures)

    def test_font_style_italic_style_block_fails(self):
        html = (
            "<html><head><style>"
            ".hint { font-style: italic; }"
            "</style></head><body></body></html>"
        )
        p = brand.BrandParser(Path("test.html"))
        p.feed(html)
        p.close()
        p.finalize()
        assert any(f["code"] == "BRAND.ITALIC" for f in p.failures)

    def test_bold_not_flagged(self):
        failures = self._parse_frag(
            "<p><strong>Bold text</strong> is fine</p>"
        )
        italic_failures = [f for f in failures if f["code"] == "BRAND.ITALIC"]
        assert not italic_failures


# ── Banned contrast pair ──────────────────────────────────────────────────────

class TestBannedPairs:
    def _parse_frag(self, frag):
        p = brand.BrandParser(Path("test.html"))
        p.feed(f"<html><body>{frag}</body></html>")
        p.close()
        p.finalize()
        return p.failures

    def test_gold_on_white_fails(self):
        failures = self._parse_frag(
            "<p style=\"color: #FFC627; background-color: #FFFFFF;\">bad</p>"
        )
        assert any(f["code"] == "BRAND.CONTRAST_PAIR" for f in failures)

    def test_white_on_gold_fails(self):
        failures = self._parse_frag(
            "<p style=\"color: #FFFFFF; background-color: #FFC627;\">bad</p>"
        )
        assert any(f["code"] == "BRAND.CONTRAST_PAIR" for f in failures)

    def test_maroon_on_black_fails(self):
        failures = self._parse_frag(
            "<p style=\"color: #8C1D40; background-color: #000000;\">bad</p>"
        )
        assert any(f["code"] == "BRAND.CONTRAST_PAIR" for f in failures)

    def test_black_on_maroon_fails(self):
        failures = self._parse_frag(
            "<p style=\"color: #000000; background-color: #8C1D40;\">bad</p>"
        )
        assert any(f["code"] == "BRAND.CONTRAST_PAIR" for f in failures)

    def test_maroon_on_white_passes(self):
        failures = self._parse_frag(
            "<p style=\"color: #8C1D40; background-color: #FFFFFF;\">ok</p>"
        )
        pair_failures = [f for f in failures if f["code"] == "BRAND.CONTRAST_PAIR"]
        assert not pair_failures

    def test_only_color_no_bg_passes(self):
        failures = self._parse_frag(
            "<p style=\"color: #FFC627;\">no bg - ok</p>"
        )
        pair_failures = [f for f in failures if f["code"] == "BRAND.CONTRAST_PAIR"]
        assert not pair_failures


# ── Off-palette (warnings) ────────────────────────────────────────────────────

class TestOffPalette:
    def test_off_palette_is_warning(self):
        p = brand.BrandParser(Path("test.html"))
        p.feed(
            "<html><body>"
            "<p style=\"color: #badbad;\">off palette</p>"
            "</body></html>"
        )
        p.close()
        p.finalize()
        # Must be warning, not failure
        assert not any(f["code"] == "BRAND.OFF_PALETTE" for f in p.failures)
        assert any(w["code"] == "BRAND.OFF_PALETTE" for w in p.warnings)

    def test_off_palette_root_var_warning(self):
        p = brand.BrandParser(Path("test.html"))
        p.feed(
            "<html><head><style>"
            ":root { --off-color: #badbad; }"
            "</style></head><body></body></html>"
        )
        p.close()
        p.finalize()
        assert any(w["code"] == "BRAND.OFF_PALETTE" for w in p.warnings)

    def test_on_palette_no_warning(self):
        p = brand.BrandParser(Path("test.html"))
        p.feed(
            "<html><body>"
            "<p style=\"color: #8C1D40;\">maroon</p>"
            "</body></html>"
        )
        p.close()
        p.finalize()
        palette_warnings = [w for w in p.warnings if w["code"] == "BRAND.OFF_PALETTE"]
        assert not palette_warnings


# ── Path exemption ────────────────────────────────────────────────────────────

class TestPathExemption:
    def test_proposal_base_exempt(self):
        path = Path("templates/mockup-html/proposal-base.html")
        assert brand._is_exempt(path)

    def test_report_html_exempt(self):
        assert brand._is_exempt(Path("templates/report-html/report-base.html"))

    def test_audit_report_exempt(self):
        assert brand._is_exempt(Path("scripts/audit_report.py"))

    def test_unified_staging_review_exempt(self):
        assert brand._is_exempt(Path("scripts/unified_staging_review.py"))

    def test_normal_page_not_exempt(self):
        assert not brand._is_exempt(Path("courses/123/staging/pages/module-1-overview.html"))

    def test_run_skips_exempt_path(self, tmp_path):
        """Files at exempt paths should be skipped; run() returns pass even with violations."""
        exempt_dir = tmp_path / "report-html"
        exempt_dir.mkdir()
        bad = exempt_dir / "bad.html"
        bad.write_text(
            "<html><body><p style=\"font-family: Roboto;\">bad font</p></body></html>",
            encoding="utf-8",
        )
        report = brand.run([str(bad)])
        assert report["pass"], f"Exempt file should be skipped; got: {report}"


# ── Clean fixture must PASS ───────────────────────────────────────────────────

class TestCleanFixture:
    def test_clean_html_passes(self):
        assert CLEAN_HTML.exists(), f"Fixture not found: {CLEAN_HTML}"
        report = _run_report(CLEAN_HTML)
        assert report["pass"], f"clean.html should pass but got failures: {report['failures']}"

    def test_clean_html_no_failures(self):
        failures, _ = _run_on(CLEAN_HTML)
        assert not failures, f"clean.html should have no failures: {failures}"


# ── Off-brand fixture must FAIL with one finding per violation type ───────────

class TestOffbrandFixture:
    def test_offbrand_html_fails(self):
        assert OFFBRAND_HTML.exists(), f"Fixture not found: {OFFBRAND_HTML}"
        report = _run_report(OFFBRAND_HTML)
        assert not report["pass"], "offbrand.html should fail brand check"

    def test_font_violation_found(self):
        failures, _ = _run_on(OFFBRAND_HTML)
        assert any(f["code"] == "BRAND.FONT" for f in failures), (
            "Expected BRAND.FONT failure for Roboto font"
        )

    def test_italic_tag_violation_found(self):
        failures, _ = _run_on(OFFBRAND_HTML)
        assert any(f["code"] == "BRAND.ITALIC" for f in failures), (
            "Expected BRAND.ITALIC failure for <i> tag"
        )

    def test_contrast_pair_violation_found(self):
        failures, _ = _run_on(OFFBRAND_HTML)
        assert any(f["code"] == "BRAND.CONTRAST_PAIR" for f in failures), (
            "Expected BRAND.CONTRAST_PAIR failure for gold-on-white"
        )

    def test_off_palette_warning_found(self):
        _, warnings = _run_on(OFFBRAND_HTML)
        assert any(w["code"] == "BRAND.OFF_PALETTE" for w in warnings), (
            "Expected BRAND.OFF_PALETTE warning for #badbad :root var"
        )

    def test_style_block_root_var_caught(self):
        """Proves the :root style-block catch is working (not just inline)."""
        _, warnings = _run_on(OFFBRAND_HTML)
        root_var_warnings = [
            w for w in warnings
            if w["code"] == "BRAND.OFF_PALETTE" and "--off-palette-color" in w["message"]
        ]
        assert root_var_warnings, "Expected :root CSS variable off-palette warning"


# ── CLI / JSON contract ───────────────────────────────────────────────────────

class TestCLI:
    def _cli(self, path):
        result = subprocess.run(
            [sys.executable, str(Path(__file__).parent.parent / "scripts" / "check_asu_brand.py"), str(path)],
            text=True,
            capture_output=True,
        )
        return result

    def test_clean_exits_zero(self):
        result = self._cli(CLEAN_HTML)
        assert result.returncode == 0, f"Expected exit 0 for clean fixture; got: {result.stderr}"

    def test_offbrand_exits_nonzero(self):
        result = self._cli(OFFBRAND_HTML)
        assert result.returncode != 0, "Expected nonzero exit for offbrand fixture"

    def test_output_is_valid_json(self):
        result = self._cli(CLEAN_HTML)
        data = json.loads(result.stdout)
        assert "pass" in data
        assert "failures" in data
        assert "warnings" in data

    def test_output_has_correct_pass_false_for_offbrand(self):
        result = self._cli(OFFBRAND_HTML)
        data = json.loads(result.stdout)
        assert data["pass"] is False
        assert data["failures"]


# ── Deploy adapter (deploy_interactives.py integration) ──────────────────────

class TestDeployAdapter:
    """Verify the brand check is wired into deploy_interactives.py."""

    def test_run_brand_check_function_exists(self):
        """deploy_interactives.py should define run_brand_check()."""
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            "deploy_interactives",
            Path(__file__).parent.parent / "scripts" / "deploy_interactives.py",
        )
        mod = importlib.util.module_from_spec(spec)
        # Don't exec — just check source
        source = (Path(__file__).parent.parent / "scripts" / "deploy_interactives.py").read_text()
        assert "run_brand_check" in source, "deploy_interactives.py must define run_brand_check()"
        assert "check_asu_brand" in source or "brand_check" in source.lower(), (
            "deploy_interactives.py must reference brand check"
        )

    def test_brand_blocking_logic_present(self):
        """Brand failures must block deploy (font/italic/contrast-pair FAILURE)."""
        source = (Path(__file__).parent.parent / "scripts" / "deploy_interactives.py").read_text()
        assert "brand" in source.lower(), "deploy_interactives.py must reference brand check"


# ── Preflight adapter ─────────────────────────────────────────────────────────

class TestPreflightAdapter:
    """Verify brand findings appear in preflight_checks.py."""

    def test_brand_criterion_id_in_preflight(self):
        source = (Path(__file__).parent.parent / "scripts" / "preflight_checks.py").read_text()
        assert "brand." in source.lower() or "BRAND." in source, (
            "preflight_checks.py must emit brand.* Issue objects"
        )
