"""Tests for display.py rendering helpers.

Each test writes to a StringIO buffer via a forced-terminal Rich Console so
we can assert on the rendered text without touching stdout.

Rich emits ANSI escape codes when force_terminal=True, which fragment the
plain text tokens we want to assert on (e.g. "print(1 + 2)" becomes several
separate colored segments).  We strip ANSI codes via `strip_ansi()` before
asserting so tests remain readable and stable across Rich versions.
"""

import re
from io import StringIO

import pytest
from rich.console import Console

from skilark_cli.display import (
    render_challenge,
    render_coding_problem,
    render_coding_stats,
    render_hint,
    render_result,
    render_stats,
    render_test_results,
)

# ANSI escape code pattern — matches CSI sequences and OSC sequences.
_ANSI_RE = re.compile(r"\x1b(?:\[[0-9;]*[mK]|\][^\x07]*\x07)")


def strip_ansi(text: str) -> str:
    """Remove all ANSI escape sequences from *text*."""
    return _ANSI_RE.sub("", text)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def make_console() -> tuple[Console, StringIO]:
    """Return a (Console, buffer) pair for capture-based assertions."""
    buf = StringIO()
    # force_terminal=True makes Rich emit styled output rather than plain text,
    # which is required for the markup to be processed at all.
    console = Console(file=buf, force_terminal=True, width=80)
    return console, buf


# ---------------------------------------------------------------------------
# render_challenge
# ---------------------------------------------------------------------------


def test_render_challenge_contains_day_and_metadata():
    console, buf = make_console()
    challenge = {
        "source": "python",
        "title": "Generator Internals",
        "code_display": "print(1 + 2)",
        "question": "What does this print?",
        "hints": ["Think about arithmetic"],
    }
    render_challenge(console, challenge, day=7)
    output = buf.getvalue()

    assert "python" in output
    assert "Generator Internals" in output
    assert "7" in output  # day number


def test_render_challenge_includes_code():
    console, buf = make_console()
    challenge = {
        "source": "python",
        "title": "Generator Internals",
        "code_display": "print(1 + 2)",
        "question": "What does this print?",
        "hints": [],
    }
    render_challenge(console, challenge, day=1)
    # Strip ANSI escape codes: syntax highlighting splits tokens with color
    # sequences, so "print(1 + 2)" would not appear as a literal substring.
    output = strip_ansi(buf.getvalue())

    assert "print(1 + 2)" in output


def test_render_challenge_includes_question():
    console, buf = make_console()
    challenge = {
        "source": "go",
        "title": "Goroutine Leaks",
        "code_display": "go func() { select {} }()",
        "question": "What is wrong here?",
        "hints": [],
    }
    render_challenge(console, challenge, day=3)
    output = buf.getvalue()

    assert "What is wrong here?" in output


def test_render_challenge_shows_key_bindings():
    console, buf = make_console()
    challenge = {
        "source": "sql",
        "title": "Window Functions",
        "code_display": "SELECT ROW_NUMBER() OVER () FROM t;",
        "question": "What does this return?",
        "hints": [],
    }
    render_challenge(console, challenge, day=10)
    output = buf.getvalue()

    # Key binding hints should appear somewhere in the output.
    assert "hint" in output.lower() or "[h]" in output
    assert "skip" in output.lower() or "[s]" in output
    assert "quit" in output.lower() or "[q]" in output


def test_render_challenge_handles_missing_optional_fields():
    """Render should not crash when optional fields are absent."""
    console, buf = make_console()
    challenge = {
        "source": "kubernetes",
        "title": "Pod Networking",
        # No code_display, question, or hints keys
    }
    render_challenge(console, challenge, day=2)
    output = buf.getvalue()
    assert "kubernetes" in output


# ---------------------------------------------------------------------------
# render_result
# ---------------------------------------------------------------------------


def test_render_result_correct_shows_confirmation():
    console, buf = make_console()
    render_result(
        console,
        correct=True,
        explanation="Simple addition.",
        deep_link="https://skilark.com/refreshers/python#generators",
    )
    output = buf.getvalue()

    assert "Correct" in output or "✓" in output


def test_render_result_correct_shows_explanation_and_link():
    console, buf = make_console()
    render_result(
        console,
        correct=True,
        explanation="Simple addition.",
        deep_link="https://skilark.com/refreshers/python#generators",
    )
    output = buf.getvalue()

    assert "Simple addition." in output
    assert "skilark.com/refreshers/python#generators" in output


def test_render_result_wrong_shows_expected_answer():
    console, buf = make_console()
    render_result(
        console,
        correct=False,
        expected_answer="3",
        explanation="Simple addition.",
        deep_link="https://skilark.com/refreshers/python#generators",
    )
    output = buf.getvalue()

    assert "3" in output


def test_render_result_wrong_without_expected_answer():
    """render_result should not crash when expected_answer is omitted."""
    console, buf = make_console()
    render_result(
        console,
        correct=False,
        explanation="Generator return values are inaccessible via print.",
        deep_link="https://skilark.com/refreshers/python#generators",
    )
    output = buf.getvalue()

    # Should still show some failure indication
    assert "Not quite" in output or "✗" in output


def test_render_result_wrong_shows_explanation_and_link():
    console, buf = make_console()
    render_result(
        console,
        correct=False,
        expected_answer="1 2",
        explanation="Generator return values are inaccessible via print.",
        deep_link="https://skilark.com/refreshers/python#generators",
    )
    output = buf.getvalue()

    assert "Generator return values" in output
    assert "skilark.com" in output


# ---------------------------------------------------------------------------
# render_stats
# ---------------------------------------------------------------------------


def test_render_stats_shows_streak_and_total():
    console, buf = make_console()
    stats = {
        "streak": 7,
        "total_completed": 47,
        "week_completed": 8,
        "week_goal": 10,
        "topic_counts": {"Python": 10, "Kubernetes": 12},
        "last_session": {
            "date": "2026-02-25",
            "topic": "Kubernetes",
            "title": "Pod Networking",
        },
    }
    render_stats(console, stats)
    output = buf.getvalue()

    assert "7" in output   # streak
    assert "47" in output  # total_completed


def test_render_stats_shows_topics():
    console, buf = make_console()
    stats = {
        "streak": 5,
        "total_completed": 20,
        "week_completed": 3,
        "week_goal": 10,
        "topic_counts": {"Python": 10, "Kubernetes": 12, "Spark": 8},
        "last_session": None,
    }
    render_stats(console, stats)
    output = buf.getvalue()

    assert "Python" in output
    assert "Kubernetes" in output
    assert "Spark" in output


def test_render_stats_shows_last_session():
    console, buf = make_console()
    stats = {
        "streak": 2,
        "total_completed": 15,
        "week_completed": 2,
        "week_goal": 10,
        "topic_counts": {},
        "last_session": {
            "date": "yesterday",
            "topic": "Kubernetes",
            "title": "Pod Networking",
        },
    }
    render_stats(console, stats)
    output = buf.getvalue()

    assert "Kubernetes" in output
    assert "Pod Networking" in output


def test_render_stats_empty():
    """Zero stats with no topics and no last session should not crash."""
    console, buf = make_console()
    stats = {
        "streak": 0,
        "total_completed": 0,
        "week_completed": 0,
        "week_goal": 10,
        "topic_counts": {},
        "last_session": None,
    }
    render_stats(console, stats)
    output = buf.getvalue()

    assert "0" in output


def test_render_stats_week_progress_bar():
    """Week progress bar numerics should be reflected in output."""
    console, buf = make_console()
    stats = {
        "streak": 1,
        "total_completed": 5,
        "week_completed": 5,
        "week_goal": 10,
        "topic_counts": {},
        "last_session": None,
    }
    render_stats(console, stats)
    # Rich highlights numbers individually, fragmenting "5/10" with escape
    # sequences; strip ANSI before asserting on the combined string.
    output = strip_ansi(buf.getvalue())

    assert "5/10" in output


# ---------------------------------------------------------------------------
# render_hint
# ---------------------------------------------------------------------------


def test_render_hint_shows_text():
    console, buf = make_console()
    render_hint(console, "Think about what print() does")
    # Rich markup treats "print()" as a potential markup tag and may split it
    # with escape codes; strip ANSI before asserting.
    output = strip_ansi(buf.getvalue())

    assert "print()" in output


def test_render_hint_shows_bulb_icon():
    console, buf = make_console()
    render_hint(console, "Consider the base case.")
    output = buf.getvalue()

    assert "💡" in output


def test_render_hint_handles_empty_text():
    """render_hint should not crash on an empty hint string."""
    console, buf = make_console()
    render_hint(console, "")
    # Just verify no exception was raised; output may be minimal.
    assert buf.getvalue() is not None


# ---------------------------------------------------------------------------
# render_coding_problem
# ---------------------------------------------------------------------------


def test_render_coding_problem_contains_title():
    console, buf = make_console()
    problem = {
        "title": "Two Sum",
        "difficulty": 1,
        "category": "dsa",
        "description": "Return indices of two numbers that add to target.",
        "starter_code_python": "def two_sum(nums, target):\n    pass\n",
    }
    render_coding_problem(console, problem, day=1)
    output = buf.getvalue()
    assert "Two Sum" in output


def test_render_coding_problem_easy_label():
    console, buf = make_console()
    problem = {
        "title": "Two Sum",
        "difficulty": 1,
        "category": "dsa",
        "description": "Find two numbers.",
        "starter_code_python": "def two_sum(nums, target):\n    pass\n",
    }
    render_coding_problem(console, problem, day=1)
    output = buf.getvalue()
    assert "Easy" in output


def test_render_coding_problem_medium_label():
    console, buf = make_console()
    problem = {
        "title": "Longest Substring",
        "difficulty": 2,
        "category": "dsa",
        "description": "Find the longest substring without repeating characters.",
        "starter_code_python": "def length_of_longest_substring(s):\n    pass\n",
    }
    render_coding_problem(console, problem, day=2)
    output = buf.getvalue()
    assert "Medium" in output


def test_render_coding_problem_hard_label():
    console, buf = make_console()
    problem = {
        "title": "Median of Two Arrays",
        "difficulty": 3,
        "category": "dsa",
        "description": "Find the median of two sorted arrays.",
        "starter_code_python": "def find_median_sorted_arrays(nums1, nums2):\n    pass\n",
    }
    render_coding_problem(console, problem, day=3)
    output = buf.getvalue()
    assert "Hard" in output


def test_render_coding_problem_includes_category():
    console, buf = make_console()
    problem = {
        "title": "Fibonacci",
        "difficulty": 1,
        "category": "dsa",
        "description": "Compute the Nth Fibonacci number.",
        "starter_code_python": "def fib(n):\n    pass\n",
    }
    render_coding_problem(console, problem, day=5)
    output = buf.getvalue()
    assert "dsa" in output.lower() or "DSA" in output


def test_render_coding_problem_shows_day_number():
    console, buf = make_console()
    problem = {
        "title": "Power of Two",
        "difficulty": 1,
        "category": "dsa",
        "description": "Is n a power of two?",
        "starter_code_python": "def is_power_of_two(n):\n    pass\n",
    }
    render_coding_problem(console, problem, day=42)
    output = buf.getvalue()
    assert "42" in output


def test_render_coding_problem_handles_missing_description():
    """Should not crash when description is absent."""
    console, buf = make_console()
    problem = {
        "title": "Empty Problem",
        "difficulty": 1,
        "category": "dsa",
    }
    render_coding_problem(console, problem, day=1)
    output = buf.getvalue()
    assert "Empty Problem" in output


# ---------------------------------------------------------------------------
# render_test_results
# ---------------------------------------------------------------------------


def test_render_test_results_all_passed_shows_summary():
    console, buf = make_console()
    result = {
        "all_passed": True,
        "passed_count": 3,
        "total_count": 3,
        "results": [
            {"case": 0, "passed": True, "expected": "3", "actual": "3", "time_ms": 1.0, "hidden": False},
            {"case": 1, "passed": True, "expected": "5", "actual": "5", "time_ms": 0.8, "hidden": False},
            {"case": 2, "passed": True, "expected": "0", "actual": "0", "time_ms": 0.9, "hidden": False},
        ],
        "error": None,
    }
    render_test_results(console, result)
    output = strip_ansi(buf.getvalue())
    assert "3/3" in output or "3 / 3" in output or ("3" in output and "passed" in output.lower())


def test_render_test_results_partial_fail_shows_summary():
    console, buf = make_console()
    result = {
        "all_passed": False,
        "passed_count": 1,
        "total_count": 3,
        "results": [
            {"case": 0, "passed": True, "expected": "3", "actual": "3", "time_ms": 1.0, "hidden": False},
            {"case": 1, "passed": False, "expected": "5", "actual": "3", "time_ms": 0.8, "hidden": False},
            {"case": 2, "passed": False, "expected": "0", "actual": "2", "time_ms": 0.9, "hidden": False},
        ],
        "error": None,
    }
    render_test_results(console, result)
    output = strip_ansi(buf.getvalue())
    assert "1/3" in output or "1 / 3" in output or ("1" in output and "3" in output)


def test_render_test_results_shows_pass_mark():
    console, buf = make_console()
    result = {
        "all_passed": True,
        "passed_count": 1,
        "total_count": 1,
        "results": [
            {"case": 0, "passed": True, "expected": "3", "actual": "3", "time_ms": 1.0, "hidden": False},
        ],
        "error": None,
    }
    render_test_results(console, result)
    output = buf.getvalue()
    assert "✓" in output or "PASS" in output.upper() or "passed" in output.lower()


def test_render_test_results_shows_fail_mark():
    console, buf = make_console()
    result = {
        "all_passed": False,
        "passed_count": 0,
        "total_count": 1,
        "results": [
            {"case": 0, "passed": False, "expected": "3", "actual": "0", "time_ms": 0.5, "hidden": False},
        ],
        "error": None,
    }
    render_test_results(console, result)
    output = buf.getvalue()
    assert "✗" in output or "FAIL" in output.upper() or "failed" in output.lower()


def test_render_test_results_hides_expected_for_hidden_cases():
    console, buf = make_console()
    result = {
        "all_passed": False,
        "passed_count": 0,
        "total_count": 1,
        "results": [
            {
                "case": 0,
                "passed": False,
                "expected": "SECRET_VALUE_99",
                "actual": "0",
                "time_ms": 0.5,
                "hidden": True,
            },
        ],
        "error": None,
    }
    render_test_results(console, result)
    output = buf.getvalue()
    # The secret expected value must NOT be shown for hidden cases
    assert "SECRET_VALUE_99" not in output


def test_render_test_results_shows_error_message():
    console, buf = make_console()
    result = {
        "all_passed": False,
        "passed_count": 0,
        "total_count": 0,
        "results": [],
        "error": "SyntaxError: invalid syntax on line 3",
    }
    render_test_results(console, result)
    output = buf.getvalue()
    assert "SyntaxError" in output or "syntax" in output.lower() or "error" in output.lower()


def test_render_test_results_timed_out_error():
    console, buf = make_console()
    result = {
        "all_passed": False,
        "passed_count": 0,
        "total_count": 0,
        "results": [],
        "error": "execution timed out",
    }
    render_test_results(console, result)
    output = buf.getvalue()
    assert "timed out" in output.lower() or "timeout" in output.lower()


# ---------------------------------------------------------------------------
# render_coding_stats
# ---------------------------------------------------------------------------


def test_render_coding_stats_shows_category_and_count():
    console, buf = make_console()
    stats = {
        "by_category": {"dsa": 10, "python": 5, "go": 2},
        "total_solved": 17,
        "streak": 3,
    }
    render_coding_stats(console, stats)
    output = buf.getvalue()
    assert "dsa" in output.lower() or "DSA" in output
    assert "17" in output or "10" in output


def test_render_coding_stats_shows_streak():
    console, buf = make_console()
    stats = {
        "by_category": {"dsa": 5},
        "total_solved": 5,
        "streak": 7,
    }
    render_coding_stats(console, stats)
    output = buf.getvalue()
    assert "7" in output


def test_render_coding_stats_handles_empty():
    """Should not crash with no categories or zero counts."""
    console, buf = make_console()
    stats = {
        "by_category": {},
        "total_solved": 0,
        "streak": 0,
    }
    render_coding_stats(console, stats)
    assert buf.getvalue() is not None
