"""Tests for the Drill screen (predict-output challenges)."""

import pytest
from unittest.mock import MagicMock

from textual.widgets import Static

from skilark_cli.tui.app import SkilarkApp


MOCK_CHALLENGE = {
    "id": "ch-1",
    "source": "python",
    "title": "List Comprehension",
    "code_display": "nums = [1, 2, 3]\nresult = [x**2 for x in nums]\nprint(result)",
    "question": "What does this code output?",
    "expected_answer": "[1, 4, 9]",
    "explanation": "List comprehension squares each element.",
    "deep_link": "https://skilark.com/refreshers/python#list-comp",
    "hints": '["Think about the ** operator.", "It applies to each element."]',
    "challenge_type": "predict_output",
    "choices": None,
}

MOCK_CHALLENGE_2 = {
    "id": "ch-2",
    "source": "python",
    "title": "Dict Keys",
    "code_display": "d = {'a': 1, 'b': 2}\nprint(list(d.keys()))",
    "question": "What does this code output?",
    "expected_answer": "['a', 'b']",
    "explanation": "dict.keys() returns the keys.",
    "deep_link": "https://skilark.com/refreshers/python#dicts",
    "hints": '["Think about what .keys() returns."]',
    "challenge_type": "predict_output",
    "choices": None,
}

MOCK_MC_CHALLENGE = {
    "id": "ch-mc",
    "source": "kubernetes",
    "title": "Pod Restart Policy",
    "code_display": "",
    "question": "What is the default restart policy for a Kubernetes Pod?",
    "expected_answer": "A",
    "explanation": "The default restart policy is Always.",
    "deep_link": "https://skilark.com/refreshers/kubernetes#restart",
    "hints": '["Think about what happens when a container crashes."]',
    "challenge_type": "multiple_choice",
    "choices": '["A) Always", "B) OnFailure", "C) Never", "D) Depends"]',
}

MOCK_STATS = {
    "total_completed": 5,
    "streak": 3,
    "week_completed": 3,
    "week_goal": 10,
    "topic_counts": {"python": 5},
}


def _make_client(challenge=None, second_challenge=None):
    client = MagicMock()
    if second_challenge:
        client.get_next_challenge.side_effect = [
            challenge or MOCK_CHALLENGE,
            second_challenge,
        ]
    else:
        client.get_next_challenge.return_value = (
            challenge if challenge is not None else MOCK_CHALLENGE
        )
    client.get_stats.return_value = MOCK_STATS
    client.complete_challenge.return_value = None
    return client


# --- AC1: Loads challenge on launch ---


@pytest.mark.asyncio
async def test_drill_loads_on_launch():
    """AC1: Drill screen fetches a challenge on app launch."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        client.get_next_challenge.assert_called_once_with(["python"])


# --- AC2: Header shows day/source/title ---


@pytest.mark.asyncio
async def test_drill_header():
    """AC2: Challenge header shows 'Day N . source . title'."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        header = app.query_one("#drill-header", Static)
        assert "Day 6" in header.content  # total_completed=5, so day=6
        assert "python" in header.content.lower()
        assert "List Comprehension" in header.content


# --- AC3: Code displayed ---


@pytest.mark.asyncio
async def test_drill_code_displayed():
    """AC3: Code block is visible with the challenge code."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        code = app.query_one("#drill-code", Static)
        # content is a Rich Syntax object; access .code for the source text
        assert "nums" in code.content.code


# --- AC4: Question shown ---


@pytest.mark.asyncio
async def test_drill_question_shown():
    """AC4: Question text is displayed."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        question = app.query_one("#drill-question", Static)
        assert "output" in question.content.lower()


# --- AC5: Answer input exists ---


@pytest.mark.asyncio
async def test_drill_answer_input():
    """AC5: Answer input is present and focusable."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        answer = app.query_one("#drill-answer")
        assert answer is not None


# --- AC6: Correct answer ---


@pytest.mark.asyncio
async def test_drill_correct_answer():
    """AC6: Correct answer shows green confirmation + explanation."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        answer = app.query_one("#drill-answer")
        answer.focus()
        answer.value = "[1, 4, 9]"
        await pilot.press("enter")
        await pilot.pause(delay=0.5)
        result = app.query_one("#drill-result", Static)
        assert "Correct" in result.content
        assert "squares" in result.content.lower()  # explanation


# --- AC7: Wrong answer ---


@pytest.mark.asyncio
async def test_drill_wrong_answer():
    """AC7: Wrong answer shows expected answer + explanation."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        answer = app.query_one("#drill-answer")
        answer.focus()
        answer.value = "wrong"
        await pilot.press("enter")
        await pilot.pause(delay=0.5)
        result = app.query_one("#drill-result", Static)
        assert "Not quite" in result.content or "[1, 4, 9]" in result.content


# --- AC8: Self-correct ---


@pytest.mark.asyncio
async def test_drill_self_correct_yes():
    """AC8: Self-correct 'y' records the answer as correct."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        answer = app.query_one("#drill-answer")
        answer.focus()
        answer.value = "wrong"
        await pilot.press("enter")
        await pilot.pause(delay=0.5)
        # Self-correct prompt should be visible
        self_correct = app.query_one("#drill-self-correct")
        assert self_correct.display is True
        await pilot.press("y")
        await pilot.pause(delay=0.5)
        # Should record as self_corrected=True
        client.complete_challenge.assert_called_once()
        call_kwargs = client.complete_challenge.call_args
        assert call_kwargs.kwargs.get("self_corrected") is True or (
            call_kwargs[1].get("self_corrected") is True
        )


@pytest.mark.asyncio
async def test_drill_self_correct_no():
    """AC8: Self-correct 'n' records the answer as incorrect."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        answer = app.query_one("#drill-answer")
        answer.focus()
        answer.value = "wrong"
        await pilot.press("enter")
        await pilot.pause(delay=0.5)
        await pilot.press("n")
        await pilot.pause(delay=0.5)
        client.complete_challenge.assert_called_once()
        call_kwargs = client.complete_challenge.call_args
        assert call_kwargs.kwargs.get("correct") is False or (
            call_kwargs[1].get("correct") is False
        )
        assert call_kwargs.kwargs.get("self_corrected") is False or (
            call_kwargs[1].get("self_corrected") is False
        )


# --- AC9: Hints ---


@pytest.mark.asyncio
async def test_drill_escape_unfocuses_input():
    """Escape unfocuses the Input so h/s/n shortcuts work."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        answer = app.query_one("#drill-answer")
        answer.focus()
        await pilot.pause()
        assert answer.has_focus
        await pilot.press("escape")
        await pilot.pause()
        assert not answer.has_focus


@pytest.mark.asyncio
async def test_drill_skip_after_escape():
    """Skip works after pressing Escape to leave the Input."""
    client = _make_client(second_challenge=MOCK_CHALLENGE_2)
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        answer = app.query_one("#drill-answer")
        answer.focus()
        await pilot.pause()
        await pilot.press("escape")  # unfocus
        await pilot.pause()
        await pilot.press("s")
        await pilot.pause(delay=0.5)
        assert client.get_next_challenge.call_count == 2


@pytest.mark.asyncio
async def test_drill_hint():
    """AC9: Pressing 'h' shows the first hint."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        await pilot.press("h")
        await pilot.pause()
        hint = app.query_one("#drill-hint", Static)
        assert "operator" in hint.content.lower()


@pytest.mark.asyncio
async def test_drill_hint_exhausted():
    """AC9: When all hints used, shows 'No more hints'."""
    client = _make_client()
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        await pilot.press("h")  # first hint
        await pilot.pause()
        await pilot.press("h")  # second hint
        await pilot.pause()
        await pilot.press("h")  # no more
        await pilot.pause()
        hint = app.query_one("#drill-hint", Static)
        assert "No more hints" in hint.content


# --- AC10: Skip ---


@pytest.mark.asyncio
async def test_drill_skip():
    """AC10: Pressing 's' skips to the next challenge (after unfocusing input)."""
    client = _make_client(second_challenge=MOCK_CHALLENGE_2)
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        await pilot.press("s")
        await pilot.pause(delay=0.5)
        assert client.get_next_challenge.call_count == 2


@pytest.mark.asyncio
async def test_drill_skip_does_not_increment_day():
    """Skip is not a completion — day counter should stay the same."""
    client = _make_client(second_challenge=MOCK_CHALLENGE_2)
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        header = app.query_one("#drill-header", Static)
        assert "Day 6" in header.content
        await pilot.press("s")
        await pilot.pause(delay=0.5)
        header = app.query_one("#drill-header", Static)
        assert "Day 6" in header.content  # still Day 6, not Day 7


# --- AC11: Next after result ---


@pytest.mark.asyncio
async def test_drill_next_after_correct():
    """AC11: After correct answer, pressing 'n' loads next challenge."""
    client = _make_client(second_challenge=MOCK_CHALLENGE_2)
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        answer = app.query_one("#drill-answer")
        answer.focus()
        answer.value = "[1, 4, 9]"
        await pilot.press("enter")
        await pilot.pause(delay=0.5)
        await pilot.press("n")
        await pilot.pause(delay=0.5)
        header = app.query_one("#drill-header", Static)
        assert "Day 7" in header.content  # incremented


# --- AC12: No challenges ---


@pytest.mark.asyncio
async def test_drill_no_challenges():
    """AC12: When no challenges available, shows empty message."""
    client = MagicMock()
    client.get_next_challenge.return_value = None
    client.get_stats.return_value = {"total_completed": 0}
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        empty = app.query_one("#drill-empty", Static)
        assert empty.display is True
        assert "No more challenges" in empty.content


# --- AC13: Multiple choice ---


@pytest.mark.asyncio
async def test_drill_multiple_choice():
    """AC13: Multiple choice challenge shows options."""
    client = _make_client(challenge=MOCK_MC_CHALLENGE)
    app = SkilarkApp(client=client, topics=["kubernetes"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        choices = app.query_one("#drill-choices", Static)
        assert "Always" in choices.content
        assert "OnFailure" in choices.content


# --- AC14: Day increments ---


@pytest.mark.asyncio
async def test_drill_day_increments():
    """AC14: Day counter increments after completing a challenge."""
    client = _make_client(second_challenge=MOCK_CHALLENGE_2)
    app = SkilarkApp(client=client, topics=["python"])
    async with app.run_test() as pilot:
        await pilot.pause(delay=0.5)
        # Day should start at 6 (total_completed=5 + 1)
        header = app.query_one("#drill-header", Static)
        assert "Day 6" in header.content

        answer = app.query_one("#drill-answer")
        answer.focus()
        answer.value = "[1, 4, 9]"
        await pilot.press("enter")
        await pilot.pause(delay=0.5)
        await pilot.press("n")
        await pilot.pause(delay=0.5)

        header = app.query_one("#drill-header", Static)
        assert "Day 7" in header.content
