import io
import json
import logging
import subprocess
import sys
import textwrap
from pathlib import Path

import pytest


REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = REPO_ROOT / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))

import check_interactive_a11y
import deploy_interactives
import generator
import staging_server


DEPLOY = REPO_ROOT / "scripts" / "deploy_interactives.py"
SKILL = REPO_ROOT / "skills" / "interactive-content" / "SKILL.md"
VALID_ORIGIN = "http://localhost:8111"


class _MockServer:
    def __init__(self, port=8111):
        self.server_address = ("127.0.0.1", port)


class HandlerHarness(staging_server.StagingHandler):
    def __init__(self, method, path, body=None, headers=None):
        payload = b""
        req_headers = dict(headers or {})
        req_headers.setdefault("Host", "127.0.0.1:8111")
        if body is not None:
            payload = json.dumps(body).encode("utf-8")
            req_headers.setdefault("Content-Type", "application/json")
            req_headers.setdefault("Content-Length", str(len(payload)))
        self.command = method
        self.path = path
        self.headers = req_headers
        self.rfile = io.BytesIO(payload)
        self.wfile = io.BytesIO()
        self.server = _MockServer()
        self.status = None
        self.response_headers = []

    def send_response(self, code, message=None):
        self.status = code

    def send_header(self, keyword, value):
        self.response_headers.append((keyword, value))

    def end_headers(self):
        pass


def request(method, path, body=None, headers=None):
    handler = HandlerHarness(method, path, body=body, headers=headers)
    getattr(handler, f"do_{method}")()
    return handler.status, handler.wfile.getvalue()


def write_interactive(root, course_id="12345", rest="m1/m1-dialog-cards", content=None):
    if content is None:
        content = """<!doctype html>
<html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body><h1>Dialog Cards</h1><button>Start</button></body></html>"""
    path = root / course_id / "staging" / "interactives" / f"{rest}.html"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")
    return path


def test_generator_default_writes_staging_interactives_and_banner(tmp_path, monkeypatch, capsys):
    content_data = tmp_path / "content_data.py"
    content_data.write_text(
        textwrap.dedent(
            """
            ALL_ACTIVITIES = {
                "m1": [
                    {
                        "type": "dialog_cards",
                        "title": "Module 1 Terms",
                        "filename": "m1-dialog-cards.html",
                        "cards": [
                            {"front": "Retrieval practice", "back": "Practice recalling information."}
                        ],
                    }
                ]
            }
            """
        ),
        encoding="utf-8",
    )

    monkeypatch.setattr(generator, "PLUGIN_ROOT", tmp_path)
    monkeypatch.setattr(sys, "argv", ["generator.py", str(content_data), "--course-id", "12345"])

    generator.main()

    output_root = tmp_path / "courses" / "12345" / "staging" / "interactives"
    html_file = output_root / "m1" / "m1-dialog-cards.html"
    stdout = capsys.readouterr().out

    assert html_file.exists()
    assert f"Generated 1 interactive HTML files at:\n{output_root}" in stdout
    assert "http://127.0.0.1:8111/courses/12345/interactives/m1/m1-dialog-cards" in stdout
    assert "file://" not in stdout
    assert "asucb-interactive-preview" not in stdout
    assert "--from-staging" in stdout


def test_deploy_from_staging_is_default_and_source_dir_overrides(tmp_path, monkeypatch):
    monkeypatch.setattr(deploy_interactives, "PLUGIN_ROOT", tmp_path)

    expected = tmp_path / "courses" / "12345" / "staging" / "interactives"
    assert deploy_interactives.resolve_source_dir(None, True, "12345") == expected.resolve()

    override = tmp_path / "custom-source"
    assert deploy_interactives.resolve_source_dir(str(override), True, "12345") == override.resolve()

    help_result = subprocess.run(
        [sys.executable, str(DEPLOY), "--help"],
        cwd=REPO_ROOT,
        text=True,
        capture_output=True,
        check=False,
    )
    assert help_result.returncode == 0
    assert "--from-staging" in help_result.stdout
    assert "--source-dir" in help_result.stdout
    assert "--output-dir" not in help_result.stdout


def test_staging_server_interactives_route_happy_path(tmp_path, monkeypatch):
    monkeypatch.setattr(staging_server, "COURSES_DIR", tmp_path)
    write_interactive(tmp_path)

    status, body = request("GET", "/courses/12345/interactives/m1/m1-dialog-cards")

    assert status == 200
    assert b"Interactive Preview" in body
    assert b"Pre-Deploy Checklist" not in body
    assert b"Dialog Cards" in body
    assert b"<iframe" in body
    assert b'sandbox="allow-scripts allow-forms"' in body
    assert b"allow-same-origin" not in body


def test_staging_server_interactives_route_blocks_traversal(tmp_path, monkeypatch):
    monkeypatch.setattr(staging_server, "COURSES_DIR", tmp_path)
    write_interactive(tmp_path)

    status, _ = request("GET", "/courses/12345/interactives/../secret")

    assert status == 404


def test_staging_server_interactives_write_obeys_origin_guard(tmp_path, monkeypatch):
    monkeypatch.setattr(staging_server, "COURSES_DIR", tmp_path)

    status, _ = request(
        "PUT",
        "/api/staging/12345/interactives/m1-dialog-cards",
        body={"html": "<h1>Updated</h1>"},
        headers={"Origin": "https://evil.example"},
    )

    assert status == 403


@pytest.mark.parametrize(
    "name,html",
    [
        (
            "missing_alt",
            """<!doctype html><html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><h1>Title</h1><img src="x.png"></body></html>""",
        ),
        (
            "missing_viewport",
            """<!doctype html><html lang="en"><head><title>x</title></head><body><h1>Title</h1></body></html>""",
        ),
        (
            "invalid_heading",
            """<!doctype html><html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><h3>Bad Start</h3></body></html>""",
        ),
        (
            "low_contrast",
            """<!doctype html><html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><h1>Title</h1><p style="color:#888;background:#ccc">Low contrast</p></body></html>""",
        ),
        (
            "positive_tabindex",
            """<!doctype html><html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><h1>Title</h1><button tabindex="1">Go</button></body></html>""",
        ),
    ],
)
def test_wcag_failures_block_deploy_gate(tmp_path, name, html):
    path = tmp_path / f"{name}.html"
    path.write_text(html, encoding="utf-8")

    report = check_interactive_a11y.run([str(path)])

    assert report["pass"] is False
    assert report["failures"]
    assert deploy_interactives.run_a11y_check(tmp_path) is False


def test_btn_disabled_warning_does_not_fail_wcag_gate(tmp_path):
    path = tmp_path / "warning.html"
    path.write_text(
        """<!doctype html><html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><h1>Title</h1><button class="btn-disabled">Unavailable</button></body></html>""",
        encoding="utf-8",
    )

    report = check_interactive_a11y.run([str(path)])

    assert report["pass"] is True
    assert report["warnings"]


def test_shared_css_has_updated_btn_disabled_contrast():
    assert ".btn-disabled { color: #6b7280; background: #e5e7eb; cursor: not-allowed; }" in generator.SHARED_CSS
    assert "color: #888" not in generator.SHARED_CSS


def test_check_interactive_a11y_cli_uses_preflight_pass_message(tmp_path):
    path = tmp_path / "valid.html"
    path.write_text(
        """<!doctype html><html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><h1>Title</h1><button>Start</button></body></html>""",
        encoding="utf-8",
    )

    result = subprocess.run(
        [sys.executable, str(SCRIPTS_DIR / "check_interactive_a11y.py"), str(path)],
        text=True,
        capture_output=True,
        check=False,
    )
    output = result.stdout + result.stderr

    assert result.returncode == 0
    assert "WCAG passed" not in output
    assert "Preflight" in output


def test_generator_singular_instruction_warns_and_falls_back(caplog):
    data = {
        "type": "sequencing",
        "title": "Legacy Schema",
        "filename": "legacy.html",
        "instruction": "Use the legacy singular field.",
        "items": [{"text": "First", "position": 1}],
        "feedback_correct": "Correct.",
        "feedback_incorrect": "Try again.",
    }

    with caplog.at_level(logging.WARNING):
        html = generator.generate_sequencing(data)

    assert "SCHEMA WARNING: use instructions (plural)" in caplog.text
    assert "Use the legacy singular field." in html


def test_skill_documents_staging_preview_preflight_approval_gate_before_deploy():
    text = SKILL.read_text(encoding="utf-8")

    assert "**Write Safety**" in text
    assert "staging-server preview is the user's review surface; do not skip it" in text
    generate_idx = text.index("1. Generate")
    preview_idx = text.index("2. Preview")
    preflight_idx = text.index("3. Preflight")
    approval_idx = text.index("4. Approval gate")
    deploy_idx = text.index("5. Deploy")
    verify_idx = text.index("6. Verify")

    assert generate_idx < preview_idx < preflight_idx < approval_idx < deploy_idx < verify_idx
    assert "Do not infer approval from silence or from \"thanks\"" in text
    assert "courses/{id}/staging/interactives/" in text
    assert "deploy_interactives.py --from-staging" in text
    assert "Preflight gate" in text
    assert "not a full conformance claim" in text
