"""
Tests for run_coding_flow — the coding problem interaction loop.

All tests use a mock client so there is no network I/O.  Editor, executor,
and display helpers are patched at the module level so no terminal input or
subprocess execution is required.
"""

from unittest.mock import MagicMock, call, patch

import pytest

from skilark_cli.commands.today import _build_editor_header, run_coding_flow


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


def make_problem(
    id: str = "cp1",
    hints: list[str] | None = None,
    explanation: str = "Use a hash map.",
) -> dict:
    """Return a minimal but valid coding problem dict."""
    return {
        "id": id,
        "slug": "two-sum",
        "title": "Two Sum",
        "description": "Given nums and target, return indices.",
        "difficulty": 1,
        "category": "arrays-and-hashing",
        "topic": "dsa",
        "languages": {
            "python": {
                "function_name": "two_sum",
                "starter_code": "def two_sum(nums, target):\n    pass\n",
            }
        },
        "test_cases": [
            {"input": {"nums": [2, 7], "target": 9}, "expected": [0, 1]},
        ],
        "hints": hints if hints is not None else ["Try a hash map"],
        "explanation": explanation,
    }


def _make_exec_result(all_passed: bool = True, passed: int = 1, total: int = 1) -> dict:
    """Return a minimal execution result dict."""
    return {
        "all_passed": all_passed,
        "passed_count": passed,
        "total_count": total,
        "results": [
            {
                "case": 0,
                "passed": all_passed,
                "expected": "[0, 1]",
                "actual": "[0, 1]" if all_passed else "[1, 0]",
                "time_ms": 0.5,
                "hidden": False,
            }
        ],
        "error": None,
    }


# ---------------------------------------------------------------------------
# Happy path: user solves on first attempt
# ---------------------------------------------------------------------------


def test_solve_all_pass():
    """User selects solve, all tests pass, submission is recorded."""
    mock_client = MagicMock()
    problem = make_problem()
    exec_result = _make_exec_result(all_passed=True)

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.open_in_editor", return_value="def two_sum(nums, target): return [0,1]"), \
         patch("skilark_cli.commands.today.build_python_harness", return_value="harness_code"), \
         patch("skilark_cli.commands.today.run_code_local", return_value=exec_result), \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.render_test_results"):

        mock_inq.select.return_value.execute.return_value = "solve"

        result = run_coding_flow(mock_client, problem, language="python", day=1)

    assert result == "done"
    mock_client.submit_coding_problem.assert_called_once_with(
        "cp1",
        language="python",
        all_passed=True,
        passed_count=1,
        total_count=1,
    )


# ---------------------------------------------------------------------------
# Skip: user skips without attempting
# ---------------------------------------------------------------------------


def test_skip():
    """User selects skip; no submission is recorded and 'done' is returned."""
    mock_client = MagicMock()
    problem = make_problem()

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.render_coding_problem"):

        mock_inq.select.return_value.execute.return_value = "skip"

        result = run_coding_flow(mock_client, problem, language="python", day=1)

    assert result == "done"
    mock_client.submit_coding_problem.assert_not_called()


# ---------------------------------------------------------------------------
# Quit: user quits immediately
# ---------------------------------------------------------------------------


def test_quit():
    """User selects quit; 'quit' is returned and no submission is made."""
    mock_client = MagicMock()
    problem = make_problem()

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.render_coding_problem"):

        mock_inq.select.return_value.execute.return_value = "quit"

        result = run_coding_flow(mock_client, problem, language="python", day=1)

    assert result == "quit"
    mock_client.submit_coding_problem.assert_not_called()


# ---------------------------------------------------------------------------
# Hint then solve
# ---------------------------------------------------------------------------


def test_hint_then_solve():
    """User requests a hint first, then solves; hint is rendered and submission recorded."""
    mock_client = MagicMock()
    problem = make_problem(hints=["Try a hash map"])
    exec_result = _make_exec_result(all_passed=True)

    # Action sequence: hint → solve
    action_sequence = ["hint", "solve"]

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.open_in_editor", return_value="def two_sum(nums, target): return [0,1]"), \
         patch("skilark_cli.commands.today.build_python_harness", return_value="harness_code"), \
         patch("skilark_cli.commands.today.run_code_local", return_value=exec_result), \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.render_test_results"), \
         patch("skilark_cli.commands.today.render_hint") as mock_render_hint:

        mock_inq.select.return_value.execute.side_effect = action_sequence

        result = run_coding_flow(mock_client, problem, language="python", day=1)

    assert result == "done"
    mock_render_hint.assert_called_once_with(
        mock_render_hint.call_args[0][0], "Try a hash map"
    )
    mock_client.submit_coding_problem.assert_called_once_with(
        "cp1",
        language="python",
        all_passed=True,
        passed_count=1,
        total_count=1,
    )


# ---------------------------------------------------------------------------
# Failed attempt then retry and solve
# ---------------------------------------------------------------------------


def test_failed_attempt_then_retry():
    """After a failed attempt, user retries and solves; attempt count is 2."""
    mock_client = MagicMock()
    problem = make_problem()

    fail_result = _make_exec_result(all_passed=False, passed=0, total=1)
    pass_result = _make_exec_result(all_passed=True, passed=1, total=1)

    action_sequence = ["solve", "retry"]

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.open_in_editor", return_value="def two_sum(nums, target): return [0,1]"), \
         patch("skilark_cli.commands.today.build_python_harness", return_value="harness_code"), \
         patch("skilark_cli.commands.today.run_code_local", side_effect=[fail_result, pass_result]), \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.render_test_results"):

        mock_inq.select.return_value.execute.side_effect = action_sequence

        result = run_coding_flow(mock_client, problem, language="python", day=1)

    assert result == "done"
    mock_client.submit_coding_problem.assert_called_once_with(
        "cp1",
        language="python",
        all_passed=True,
        passed_count=1,
        total_count=1,
    )


# ---------------------------------------------------------------------------
# Hint after failed attempt
# ---------------------------------------------------------------------------


def test_hint_after_failed_attempt_then_retry():
    """After a failed attempt, user gets a hint, then retries and solves."""
    mock_client = MagicMock()
    problem = make_problem(hints=["Try a hash map", "Use enumerate"])

    fail_result = _make_exec_result(all_passed=False, passed=0, total=1)
    pass_result = _make_exec_result(all_passed=True, passed=1, total=1)

    # fail → hint → retry (solve)
    action_sequence = ["solve", "hint", "retry"]

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.open_in_editor", return_value="def two_sum(nums, target): return [0,1]"), \
         patch("skilark_cli.commands.today.build_python_harness", return_value="harness_code"), \
         patch("skilark_cli.commands.today.run_code_local", side_effect=[fail_result, pass_result]), \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.render_test_results"), \
         patch("skilark_cli.commands.today.render_hint") as mock_render_hint:

        mock_inq.select.return_value.execute.side_effect = action_sequence

        result = run_coding_flow(mock_client, problem, language="python", day=1)

    assert result == "done"
    # Hint should have been rendered once.
    mock_render_hint.assert_called_once()
    # Submission should record the successful retry.
    mock_client.submit_coding_problem.assert_called_once()


# ---------------------------------------------------------------------------
# Test case normalisation: JSON string inputs
# ---------------------------------------------------------------------------


def test_json_string_fields_are_parsed():
    """Problem fields arriving as JSON strings are parsed before use."""
    mock_client = MagicMock()
    problem = {
        "id": "cp2",
        "title": "Add Two",
        "description": "Return sum.",
        "difficulty": 1,
        "category": "math",
        "topic": "dsa",
        "languages": '{"python": {"function_name": "add", "starter_code": "def add(a, b):\\n    pass\\n"}}',
        "test_cases": '[{"input": {"a": 1, "b": 2}, "expected": 3}]',
        "hints": '["Think about +"]',
        "explanation": "Just return a+b.",
    }
    exec_result = _make_exec_result(all_passed=True)

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.open_in_editor", return_value="def add(a,b): return a+b"), \
         patch("skilark_cli.commands.today.build_python_harness", return_value="harness") as mock_harness, \
         patch("skilark_cli.commands.today.run_code_local", return_value=exec_result), \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.render_test_results"):

        mock_inq.select.return_value.execute.return_value = "solve"

        result = run_coding_flow(mock_client, problem, language="python", day=2)

    assert result == "done"
    # Verify harness was built with the correct function name extracted from JSON
    mock_harness.assert_called_once()
    call_args = mock_harness.call_args
    assert call_args[0][1] == "add"  # function_name positional arg


# ---------------------------------------------------------------------------
# Submission failure: API error should not crash the flow
# ---------------------------------------------------------------------------


def test_submission_failure_does_not_crash():
    """API failure during submission should not crash the flow."""
    mock_client = MagicMock()
    mock_client.submit_coding_problem.side_effect = Exception("offline")
    problem = make_problem()
    exec_result = _make_exec_result(all_passed=True)

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.open_in_editor", return_value="def two_sum(nums, target): return [0,1]"), \
         patch("skilark_cli.commands.today.build_python_harness", return_value="harness_code"), \
         patch("skilark_cli.commands.today.run_code_local", return_value=exec_result), \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.render_test_results"):

        mock_inq.select.return_value.execute.return_value = "solve"

        result = run_coding_flow(mock_client, problem, language="python", day=1)

    # Flow completes gracefully despite the API error.
    assert result == "done"


# ---------------------------------------------------------------------------
# Non-Python language: should gate early with a clear message
# ---------------------------------------------------------------------------


def test_non_python_language_gates_before_editor():
    """Selecting a non-Python language shows an unsupported message and returns done."""
    mock_client = MagicMock()
    problem = make_problem()

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.open_in_editor") as mock_editor:

        mock_inq.select.return_value.execute.return_value = "solve"

        result = run_coding_flow(mock_client, problem, language="java", day=1)

    assert result == "done"
    # The editor must NOT be opened for unsupported languages.
    mock_editor.assert_not_called()
    # No submission should be attempted.
    mock_client.submit_coding_problem.assert_not_called()


# ---------------------------------------------------------------------------
# Partial pass then skip/quit: partial result should still be submitted
# ---------------------------------------------------------------------------


def test_partial_pass_then_skip_submits_result():
    """After a partial pass, skipping should still submit the partial result."""
    mock_client = MagicMock()
    problem = make_problem()

    fail_result = _make_exec_result(all_passed=False, passed=3, total=5)

    action_sequence = ["solve", "skip"]

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.open_in_editor", return_value="code"), \
         patch("skilark_cli.commands.today.build_python_harness", return_value="harness"), \
         patch("skilark_cli.commands.today.run_code_local", return_value=fail_result), \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.render_test_results"):

        mock_inq.select.return_value.execute.side_effect = action_sequence

        result = run_coding_flow(mock_client, problem, language="python", day=1)

    assert result == "done"
    mock_client.submit_coding_problem.assert_called_once_with(
        "cp1",
        language="python",
        all_passed=False,
        passed_count=3,
        total_count=5,
    )


def test_partial_pass_then_quit_submits_result():
    """After a partial pass, quitting should still submit the partial result."""
    mock_client = MagicMock()
    problem = make_problem()

    fail_result = _make_exec_result(all_passed=False, passed=2, total=5)

    action_sequence = ["solve", "quit"]

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.open_in_editor", return_value="code"), \
         patch("skilark_cli.commands.today.build_python_harness", return_value="harness"), \
         patch("skilark_cli.commands.today.run_code_local", return_value=fail_result), \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.render_test_results"):

        mock_inq.select.return_value.execute.side_effect = action_sequence

        result = run_coding_flow(mock_client, problem, language="python", day=1)

    assert result == "quit"
    mock_client.submit_coding_problem.assert_called_once_with(
        "cp1",
        language="python",
        all_passed=False,
        passed_count=2,
        total_count=5,
    )


# ---------------------------------------------------------------------------
# Editor header: problem context prepended to starter code
# ---------------------------------------------------------------------------


def test_build_editor_header_includes_title_and_examples():
    """Header should contain the problem title and visible test case examples."""
    problem = make_problem()
    test_cases_raw = [
        {"input": {"nums": [2, 7], "target": 9}, "expected": [0, 1]},
        {"input": {"nums": [3, 3], "target": 6}, "expected": [0, 1], "hidden": True},
    ]
    header = _build_editor_header(problem, test_cases_raw, "python")

    assert header.startswith("# Two Sum")
    # Should contain the visible example but not the hidden one.
    assert "nums=[2, 7], target=9 -> [0, 1]" in header
    assert "[3, 3]" not in header
    # Every line should be a comment or blank.
    for line in header.splitlines():
        assert line == "" or line.startswith("#"), f"Non-comment line: {line!r}"


def test_build_editor_header_java_uses_double_slash():
    """Java header should use // comment prefix."""
    problem = make_problem()
    test_cases_raw = [{"input": {"nums": [1]}, "expected": 1}]
    header = _build_editor_header(problem, test_cases_raw, "java")

    assert header.startswith("// Two Sum")
    for line in header.splitlines():
        assert line == "" or line.startswith("//"), f"Non-comment line: {line!r}"


def test_editor_receives_header_plus_starter_code():
    """open_in_editor should receive the header prepended to the starter code."""
    mock_client = MagicMock()
    problem = make_problem()
    exec_result = _make_exec_result(all_passed=True)

    with patch("skilark_cli.commands.today.inquirer") as mock_inq, \
         patch("skilark_cli.commands.today.open_in_editor", return_value="def two_sum(nums, target): return [0,1]") as mock_editor, \
         patch("skilark_cli.commands.today.build_python_harness", return_value="harness_code"), \
         patch("skilark_cli.commands.today.run_code_local", return_value=exec_result), \
         patch("skilark_cli.commands.today.render_coding_problem"), \
         patch("skilark_cli.commands.today.render_test_results"):

        mock_inq.select.return_value.execute.return_value = "solve"

        run_coding_flow(mock_client, problem, language="python", day=1)

    # The first argument to open_in_editor should start with the header comment.
    editor_input = mock_editor.call_args[0][0]
    assert editor_input.startswith("# Two Sum")
    # And should end with the starter code.
    assert "def two_sum(nums" in editor_input
