"""Tests for PR Spec 1 — Hybrid Quiz Details/Questions Tabs.

Covers:
  1. render_quiz_details_card outputs all expected chips for a sample quiz
  2. render_quiz_details_card omits Unpublished chip when published=True
  3. GET /api/staging/.../details returns the quiz block
  4. PUT /api/staging/.../details accepts a valid payload and writes back
  5. PUT rejects invalid quiz_type value
  6. PUT rejects unknown payload keys
  7. Preflight catches redundant_scoring_policy
  8. Preflight catches time_limit_on_practice
  9. Tab HTML contains both Details + Questions tab buttons + correct aria-selected on default
 10. URL hash #details makes Details the active tab
"""
from __future__ import annotations

import io
import json
import pathlib
import re
import sys

import pytest

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

import preflight_checks
import staging_server
import unified_staging_review


VALID_ORIGIN = "http://127.0.0.1:8111"


# ── HandlerHarness reused pattern from test_staging_server.py ────────────────

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}")()
    body_bytes = handler.wfile.getvalue()
    try:
        decoded = json.loads(body_bytes.decode("utf-8")) if body_bytes else None
    except json.JSONDecodeError:
        decoded = None
    return handler.status, decoded


@pytest.fixture()
def courses_root(tmp_path, monkeypatch):
    monkeypatch.setattr(staging_server, "COURSES_DIR", tmp_path)
    return tmp_path


def _write_quiz(root, course_id="course1", obj_id="quiz1", quiz=None, questions=None):
    quiz = quiz or {
        "title": "Sample Quiz",
        "quiz_type": "assignment",
        "points_possible": 10,
        "allowed_attempts": 2,
        "time_limit": None,
        "scoring_policy": "keep_highest",
        "shuffle_answers": True,
        "published": False,
    }
    payload = {"quiz": quiz, "questions": questions or []}
    path = root / course_id / "staging" / "quizzes" / f"{obj_id}.json"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
    return path


# ── 1. render_quiz_details_card emits all expected chips ────────────────────

def test_quiz_details_card_renders_all_chips():
    quiz_data = {
        "quiz": {
            "title": "Module 3 Quiz",
            "quiz_type": "assignment",
            "points_possible": 12,
            "allowed_attempts": 3,
            "time_limit": 30,
            "scoring_policy": "keep_highest",
            "shuffle_answers": True,
            "published": False,
        }
    }
    html = unified_staging_review.render_quiz_details_card(quiz_data, edit_url="/courses/c/quizzes/q1")
    assert "Graded Quiz" in html
    assert "12 points" in html
    assert "3 attempts" in html
    assert "30 min" in html
    assert "Keep highest" in html
    assert "Shuffle: on" in html
    assert "Unpublished" in html
    # Brand colors
    assert "#4E4E4E" in html  # slate
    assert "#FFC627" in html  # gold
    assert "#8C1D40" in html  # maroon
    assert "#B91C1C" in html  # red for unpublished
    # Chips link to the edit page with #details hash
    assert "#details" in html
    assert "/courses/c/quizzes/q1#details" in html


# ── 2. Unpublished chip omitted when published=True ─────────────────────────

def test_quiz_details_card_omits_unpublished_when_published():
    quiz_data = {
        "quiz": {
            "title": "Published Quiz",
            "quiz_type": "practice_quiz",
            "points_possible": 0,
            "allowed_attempts": -1,
            "time_limit": None,
            "scoring_policy": "keep_latest",
            "shuffle_answers": False,
            "published": True,
        }
    }
    html = unified_staging_review.render_quiz_details_card(quiz_data, edit_url="/x")
    assert "Unpublished" not in html
    # And no red color either
    assert "#B91C1C" not in html
    assert "Practice Quiz" in html
    assert "Unlimited attempts" in html
    assert "No time limit" in html
    assert "Keep latest" in html
    assert "Shuffle: off" in html


# ── 3. GET /details returns the quiz block ──────────────────────────────────

def test_get_quiz_details(courses_root):
    _write_quiz(courses_root)
    status, body = request("GET", "/api/staging/course1/quizzes/quiz1/details")
    assert status == 200
    assert body["ok"] is True
    assert body["data"]["title"] == "Sample Quiz"
    assert body["data"]["quiz_type"] == "assignment"
    assert body["data"]["allowed_attempts"] == 2


# ── 4. PUT /details accepts valid payload + writes back ─────────────────────

def test_put_quiz_details_valid(courses_root):
    path = _write_quiz(courses_root)
    payload = {
        "title": "Renamed Quiz",
        "quiz_type": "practice_quiz",
        "allowed_attempts": -1,
        "scoring_policy": "keep_latest",
        "shuffle_answers": False,
        "published": True,
    }
    status, body = request(
        "PUT",
        "/api/staging/course1/quizzes/quiz1/details",
        body=payload,
        headers={"Origin": VALID_ORIGIN},
    )
    assert status == 200, body
    assert body["ok"] is True
    on_disk = json.loads(path.read_text())
    assert on_disk["quiz"]["title"] == "Renamed Quiz"
    assert on_disk["quiz"]["quiz_type"] == "practice_quiz"
    assert on_disk["quiz"]["allowed_attempts"] == -1
    assert on_disk["quiz"]["published"] is True
    # Questions array preserved
    assert "questions" in on_disk


# ── 5. PUT rejects invalid quiz_type ────────────────────────────────────────

def test_put_quiz_details_rejects_invalid_quiz_type(courses_root):
    _write_quiz(courses_root)
    payload = {
        "title": "X",
        "quiz_type": "graded_survey",  # not in allowed set
        "allowed_attempts": 1,
        "scoring_policy": "keep_highest",
        "shuffle_answers": True,
        "published": False,
    }
    status, body = request(
        "PUT",
        "/api/staging/course1/quizzes/quiz1/details",
        body=payload,
        headers={"Origin": VALID_ORIGIN},
    )
    assert status == 400
    assert body["ok"] is False
    assert any("quiz_type" in e["location"] for e in body.get("errors", []))


# ── 6. PUT rejects unknown payload keys ─────────────────────────────────────

def test_put_quiz_details_rejects_unknown_keys(courses_root):
    _write_quiz(courses_root)
    payload = {
        "title": "X",
        "quiz_type": "assignment",
        "allowed_attempts": 1,
        "scoring_policy": "keep_highest",
        "shuffle_answers": True,
        "published": False,
        "ttle": "typo-key",  # typo should be rejected
    }
    status, body = request(
        "PUT",
        "/api/staging/course1/quizzes/quiz1/details",
        body=payload,
        headers={"Origin": VALID_ORIGIN},
    )
    assert status == 400
    assert body["ok"] is False
    assert any(e["code"] == "unknown_key" for e in body.get("errors", []))


# ── 7. Preflight catches redundant_scoring_policy ───────────────────────────

def test_preflight_redundant_scoring_policy():
    issues = preflight_checks.check_quiz_details_consistency({
        "title": "Q",
        "quiz_type": "assignment",
        "allowed_attempts": 1,
        "scoring_policy": "keep_highest",
        "published": False,
        "points_possible": 5,
    })
    codes = [i.criterion_id for i in issues]
    assert "redundant_scoring_policy" in codes
    # Also verify the average variant
    issues2 = preflight_checks.check_quiz_details_consistency({
        "title": "Q",
        "quiz_type": "assignment",
        "allowed_attempts": 1,
        "scoring_policy": "take_average",
        "published": False,
        "points_possible": 5,
    })
    assert any(i.criterion_id == "redundant_scoring_policy" for i in issues2)


# ── 8. Preflight catches time_limit_on_practice ─────────────────────────────

def test_preflight_time_limit_on_practice():
    issues = preflight_checks.check_quiz_details_consistency({
        "title": "Q",
        "quiz_type": "practice_quiz",
        "allowed_attempts": -1,
        "scoring_policy": "keep_latest",
        "time_limit": 20,
        "shuffle_answers": False,
        "published": True,
        "points_possible": 0,
    })
    codes = [i.criterion_id for i in issues]
    assert "time_limit_on_practice" in codes
    # Also published_no_points fires here
    assert "published_no_points" in codes


# ── 9. Tab HTML contains both buttons + aria-selected on Questions default ──

def test_quiz_edit_page_has_tabs(courses_root):
    _write_quiz(courses_root)
    # Render the preview HTML for the quiz
    quiz_data = json.loads((courses_root / "course1" / "staging" / "quizzes" / "quiz1.json").read_text())
    rendered = staging_server._render_quiz(quiz_data, title="Quiz", course_id="course1", obj_id="quiz1")
    assert 'id="quiz-tab-btn-details"' in rendered
    assert 'id="quiz-tab-btn-questions"' in rendered
    # Default = Questions selected
    m_det = re.search(r'id="quiz-tab-btn-details"[^>]*?aria-selected="([^"]+)"', rendered)
    m_q = re.search(r'id="quiz-tab-btn-questions"[^>]*?aria-selected="([^"]+)"', rendered)
    assert m_det and m_det.group(1) == "false"
    assert m_q and m_q.group(1) == "true"
    # Details panel present + hidden by default
    assert 'id="quiz-tab-details"' in rendered
    assert 'id="quiz-tab-questions"' in rendered


# ── 10. URL hash #details activates Details tab ─────────────────────────────

def test_url_hash_details_activates_details_tab(courses_root):
    _write_quiz(courses_root)
    quiz_data = json.loads((courses_root / "course1" / "staging" / "quizzes" / "quiz1.json").read_text())
    rendered = staging_server._render_quiz(quiz_data, title="Quiz", course_id="course1", obj_id="quiz1")
    # The JS reads window.location.hash; verify the hash-to-tab logic is present.
    assert "window.location.hash" in rendered
    assert "'details'" in rendered
    # activate(fromHash()) call wires hash → tab on load
    assert "activate(fromHash())" in rendered
