"""
Tests for the today command — core challenge loop.

All tests call `run_challenge_loop()` directly with a mock client so there is
no network I/O or filesystem access.  Rich prompts are patched at the module
level so no terminal input is required.
"""

from unittest.mock import MagicMock, call, patch

import pytest

from skilark_cli.commands.today import run_challenge_loop


# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------


def make_challenge(id: str = "ch1", hints: list[str] | None = None) -> dict:
    """Return a minimal but valid challenge dict."""
    return {
        "id": id,
        "topic": "python",
        "title": "Generator Internals",
        "code_display": "print(1 + 2)",
        "question": "What does this print?",
        "expected_answer": "3",
        "explanation": "Simple addition.",
        "hints": hints if hints is not None else ["Think about math"],
        "deep_link": "https://skilark.com/refreshers/python#generators",
    }


# ---------------------------------------------------------------------------
# Core answer paths
# ---------------------------------------------------------------------------


def test_correct_answer():
    """Correct answer marks complete_challenge with correct=True, self_corrected=False."""
    mock_client = MagicMock()
    mock_client.get_next_challenge.return_value = make_challenge()

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt, \
         patch("skilark_cli.commands.today.Confirm") as mock_confirm:
        mock_prompt.ask.return_value = "3"
        mock_confirm.ask.return_value = False  # don't continue

        run_challenge_loop(mock_client, topics=["python"], day=1)

    mock_client.complete_challenge.assert_called_once_with(
        "ch1", answer="3", correct=True, self_corrected=False,
    )


def test_wrong_answer_no_self_correct():
    """Wrong answer with no self-correction records correct=False, self_corrected=False."""
    mock_client = MagicMock()
    mock_client.get_next_challenge.return_value = make_challenge()

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt, \
         patch("skilark_cli.commands.today.Confirm") as mock_confirm:
        mock_prompt.ask.return_value = "4"
        # First Confirm: "Were you actually right?" → No
        # Second Confirm: "Another?" → No
        mock_confirm.ask.side_effect = [False, False]

        run_challenge_loop(mock_client, topics=["python"], day=1)

    mock_client.complete_challenge.assert_called_once_with(
        "ch1", answer="4", correct=False, self_corrected=False,
    )


def test_wrong_answer_self_correct():
    """Self-correction flips correct to True while preserving self_corrected=True."""
    mock_client = MagicMock()
    mock_client.get_next_challenge.return_value = make_challenge()

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt, \
         patch("skilark_cli.commands.today.Confirm") as mock_confirm:
        mock_prompt.ask.return_value = "4"
        # First Confirm: "Were you actually right?" → Yes (self-correct)
        # Second Confirm: "Another?" → No
        mock_confirm.ask.side_effect = [True, False]

        run_challenge_loop(mock_client, topics=["python"], day=1)

    mock_client.complete_challenge.assert_called_once_with(
        "ch1", answer="4", correct=True, self_corrected=True,
    )


# ---------------------------------------------------------------------------
# Navigation commands
# ---------------------------------------------------------------------------


def test_no_challenges_available():
    """When the client returns None the loop exits without calling complete."""
    mock_client = MagicMock()
    mock_client.get_next_challenge.return_value = None

    run_challenge_loop(mock_client, topics=["python"], day=1)

    mock_client.complete_challenge.assert_not_called()


def test_skip():
    """'s' skips the challenge without recording a completion."""
    mock_client = MagicMock()
    mock_client.get_next_challenge.return_value = make_challenge()

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt, \
         patch("skilark_cli.commands.today.Confirm") as mock_confirm:
        mock_prompt.ask.return_value = "s"
        mock_confirm.ask.return_value = False  # don't continue after skip

        run_challenge_loop(mock_client, topics=["python"], day=1)

    mock_client.complete_challenge.assert_not_called()


def test_quit():
    """'q' exits the loop immediately without recording a completion."""
    mock_client = MagicMock()
    mock_client.get_next_challenge.return_value = make_challenge()

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt:
        mock_prompt.ask.return_value = "q"

        run_challenge_loop(mock_client, topics=["python"], day=1)

    mock_client.complete_challenge.assert_not_called()


# ---------------------------------------------------------------------------
# Hint handling
# ---------------------------------------------------------------------------


def test_hint_then_correct_answer():
    """'h' displays a hint; next non-command answer is evaluated normally."""
    mock_client = MagicMock()
    mock_client.get_next_challenge.return_value = make_challenge(
        hints=["Think about addition"]
    )

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt, \
         patch("skilark_cli.commands.today.Confirm") as mock_confirm, \
         patch("skilark_cli.commands.today.render_hint") as mock_render_hint:
        # First ask → hint; second ask → correct answer
        mock_prompt.ask.side_effect = ["h", "3"]
        mock_confirm.ask.return_value = False  # don't continue

        run_challenge_loop(mock_client, topics=["python"], day=1)

    mock_render_hint.assert_called_once()
    mock_client.complete_challenge.assert_called_once_with(
        "ch1", answer="3", correct=True, self_corrected=False,
    )


def test_hint_exhausted_then_answer():
    """Requesting hints beyond available count does not crash; answer still works."""
    mock_client = MagicMock()
    # One hint only
    mock_client.get_next_challenge.return_value = make_challenge(hints=["Only hint"])

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt, \
         patch("skilark_cli.commands.today.Confirm") as mock_confirm:
        # h → uses hint; h again → no more hints (no crash); then answer
        mock_prompt.ask.side_effect = ["h", "h", "3"]
        mock_confirm.ask.return_value = False

        run_challenge_loop(mock_client, topics=["python"], day=1)

    mock_client.complete_challenge.assert_called_once_with(
        "ch1", answer="3", correct=True, self_corrected=False,
    )


def test_hints_as_json_string():
    """Hints stored as a JSON string are parsed and rendered correctly."""
    challenge = make_challenge()
    challenge["hints"] = '["Parsed hint"]'

    mock_client = MagicMock()
    mock_client.get_next_challenge.return_value = challenge

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt, \
         patch("skilark_cli.commands.today.Confirm") as mock_confirm, \
         patch("skilark_cli.commands.today.render_hint") as mock_render_hint:
        mock_prompt.ask.side_effect = ["h", "3"]
        mock_confirm.ask.return_value = False

        run_challenge_loop(mock_client, topics=["python"], day=1)

    mock_render_hint.assert_called_once_with(mock_render_hint.call_args[0][0], "Parsed hint")


# ---------------------------------------------------------------------------
# Multi-challenge session
# ---------------------------------------------------------------------------


def test_multiple_challenges_day_increments():
    """Day counter increments across consecutive challenges in a session."""
    mock_client = MagicMock()
    mock_client.get_next_challenge.side_effect = [
        make_challenge(id="ch1"),
        make_challenge(id="ch2"),
        None,  # exhausted after two
    ]

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt, \
         patch("skilark_cli.commands.today.Confirm") as mock_confirm, \
         patch("skilark_cli.commands.today.render_challenge") as mock_render:
        mock_prompt.ask.return_value = "3"
        mock_confirm.ask.return_value = True  # keep going; pool exhausts naturally

        run_challenge_loop(mock_client, topics=["python"], day=5)

    # render_challenge should have been called with day=5, then day=6
    calls = mock_render.call_args_list
    assert calls[0] == call(mock_render.call_args_list[0][0][0], make_challenge(id="ch1"), day=5)
    assert calls[1] == call(mock_render.call_args_list[1][0][0], make_challenge(id="ch2"), day=6)

    assert mock_client.complete_challenge.call_count == 2


def test_stop_after_one():
    """Answering 'No' to 'Another?' ends the loop after a single challenge."""
    mock_client = MagicMock()
    mock_client.get_next_challenge.return_value = make_challenge()

    with patch("skilark_cli.commands.today.Prompt") as mock_prompt, \
         patch("skilark_cli.commands.today.Confirm") as mock_confirm:
        mock_prompt.ask.return_value = "3"
        mock_confirm.ask.return_value = False  # don't continue

        run_challenge_loop(mock_client, topics=["python"], day=1)

    # get_next_challenge called exactly once; second call never happens
    mock_client.get_next_challenge.assert_called_once()
    mock_client.complete_challenge.assert_called_once()
