"""Interactive chat tests using fake models on the real server.

Tests that `navari chat` correctly displays agent messages, tool cards,
and prompts for user input via stdin/stdout, using special commands
that activate fake models on the server.

Requires:
1. A running Navari server with test models enabled (DEV)
2. Valid credentials (run `navari login <url>` first)

Run with: uv run pytest cli/tests/integration/test_chat_interactive.py -v -s
"""

import json
import os
import subprocess

import pytest
from navari_cli.cli import app
from typer.testing import CliRunner

pytestmark = [pytest.mark.usefixtures("_require_server"), pytest.mark.flaky(reruns=1)]

runner = CliRunner()

# Environment for subprocess: force UTF-8 output and disable Rich formatting
# to avoid Windows cp1252 encoding errors with Unicode characters (e.g. checkmarks)
_SUBPROCESS_ENV = {**os.environ, "PYTHONIOENCODING": "utf-8", "NO_COLOR": "1"}

_SUBPROCESS_TIMEOUT = 120


def _run_chat(args: list[str], stdin_input: str, **kwargs) -> subprocess.CompletedProcess:
    """Run `navari chat` as a subprocess, capturing output.

    Wraps subprocess.run to provide diagnostic output on timeout.
    On TimeoutExpired, the partial stdout/stderr are included in the
    assertion message so CI logs show where the CLI got stuck.
    """
    try:
        return subprocess.run(
            args,
            input=stdin_input,
            capture_output=True,
            encoding="utf-8",
            timeout=_SUBPROCESS_TIMEOUT,
            env=_SUBPROCESS_ENV,
            **kwargs,
        )
    except subprocess.TimeoutExpired as exc:
        stdout = exc.stdout or b""
        stderr = exc.stderr or b""
        if isinstance(stdout, bytes):
            stdout = stdout.decode("utf-8", errors="replace")
        if isinstance(stderr, bytes):
            stderr = stderr.decode("utf-8", errors="replace")
        pytest.fail(
            f"CLI subprocess timed out after {_SUBPROCESS_TIMEOUT}s.\n"
            f"Command: {exc.cmd}\n"
            f"Partial output:\n{stdout}{stderr}"
        )


@pytest.fixture
def _require_server(server_url):
    """Skip if no server configured."""


class TestChatAskTools:
    """Test interactive chat with :test_ask_tools fake model via real CLI process.

    The :test_ask_tools fake model sends the following tool calls:
    1. send_message_to_user: "Hello user, let's mock some questions!"
    2. ask_user: "What do you want to know?"  -> waits for input
    3. send_message_to_user: "ok next question:"
    4. ask_user_with_examples: "select an option" -> waits for input
    5. ask_user_for_confirmation: "Would you like to continue?" -> waits for input
    6. final_result: "thank you"

    Tests run the CLI as a subprocess with stdin piped, simulating
    a user typing messages in the REPL.
    """

    def test_ask_tools_full_interactive(self):
        """Verifies:
        - "Created conversation" status with title and ID
        - send_message_to_user content displayed (no tool card)
        - Tool cards for ask_user, ask_user_with_examples, ask_user_for_confirmation
        - ask_user_with_examples shows numbered examples
        - final_result tool card and summary
        """
        stdin_input = (
            "\n".join(
                [
                    ":test_ask_tools",
                    "I want to know about weather",
                    "first option",
                    "yes",
                    "/quit",
                ]
            )
            + "\n"
        )

        result = _run_chat(
            ["navari", "chat", "-n", "-t", "CLI Ask Tools Interactive"],
            stdin_input,
        )

        output = result.stdout + result.stderr

        # Verify conversation was created
        assert "Created conversation" in output, f"Missing conversation creation in:\n{output}"

        # Verify send_message_to_user content is displayed (no tool card for this one)
        assert "Hello user" in output, f"Missing greeting in:\n{output}"

        # Verify tool cards are shown for ask_user tools (consistent with frontend)
        assert 'use tool "ask_user"' in output, f"Missing ask_user tool card in:\n{output}"
        assert 'use tool "ask_user_with_examples"' in output, f"Missing ask_user_with_examples tool card in:\n{output}"
        assert 'use tool "ask_user_for_confirmation"' in output, (
            f"Missing ask_user_for_confirmation tool card in:\n{output}"
        )

        # Verify ask_user question is displayed
        assert "What do you want to know" in output, f"Missing ask_user question in:\n{output}"

        # Verify ask_user_with_examples shows examples
        assert "first available option" in output, f"Missing example 1 in:\n{output}"
        assert "second option" in output, f"Missing example 2 in:\n{output}"

        # Verify ask_user_for_confirmation question
        assert "Would you like to continue" in output, f"Missing confirmation question in:\n{output}"

        # Verify final_result tool card and summary
        assert 'use tool "final_result"' in output, f"Missing final_result tool card in:\n{output}"
        assert "thank you" in output.lower(), f"Missing final result in:\n{output}"


class TestChatTodoList:
    """Test interactive chat with :test_todo_list fake model.

    The :test_todo_list model calls send_message_to_user and write_to_do_file
    multiple times, then final_result. Verifies that the CLI displays all
    tool cards and agent messages correctly.
    """

    # Expected strings that should appear in both chat output and stored messages
    EXPECTED = [
        "Starting authentication system implementation",
        "Created task list",
        "write_to_do_file",
        "Build a User Authentication System",
        "Design authentication architecture",
        "Authentication system implementation completed",
    ]

    def test_todo_list_tool_display(self):
        """Verifies:
        - "Joining" status shows title and conversation ID
        - Tool cards and agent messages are displayed correctly
        - Todo list summary with done count is rendered
        - "Ending chat session" includes conversation ID
        - Auto-rename: conversation title derived from first message
        - `conversations messages` renders the same data as live chat
        """
        # Create conversation with default title
        result = runner.invoke(app, ["conversations", "create", "--format", "json"])
        assert result.exit_code == 0
        conv = json.loads(result.output)
        conv_id = conv["id"]
        assert conv["title"] == "New Conversation"

        # Run chat via subprocess against the created conversation
        stdin_input = ":test_todo_list\n/quit\n"

        chat_result = _run_chat(
            ["navari", "chat", conv_id],
            stdin_input,
        )

        chat_output = chat_result.stdout + chat_result.stderr

        # Verify "Joining" status shows title and ID
        assert f"Joining: New Conversation ({conv_id})" in chat_output, (
            f"Missing 'Joining' status in chat output:\n{chat_output}"
        )

        # Verify expected content in live chat output
        for expected in self.EXPECTED:
            assert expected in chat_output, f"Missing '{expected}' in chat output:\n{chat_output}"

        # Chat-only assertions (not in stored messages)
        assert "done" in chat_output, f"Missing done count in chat output:\n{chat_output}"

        # Verify "Ending" message includes conversation ID
        assert f"Ending chat session ({conv_id})" in chat_output, (
            f"Missing conversation ID in ending message:\n{chat_output}"
        )

        # Verify auto-rename: title should be derived from ":test_todo_list"
        result = runner.invoke(app, ["conversations", "get", conv_id, "--format", "json"])
        assert result.exit_code == 0
        conv = json.loads(result.output)
        assert conv["title"] == ":test_todo_list", (
            f"Expected auto-renamed title ':test_todo_list', got '{conv['title']}'"
        )

        # Verify the same data renders in `conversations messages` (plain format)
        msg_result = runner.invoke(app, ["conversations", "messages", conv_id])
        assert msg_result.exit_code == 0, f"messages command failed:\n{msg_result.output}"
        msg_output = msg_result.output

        for expected in self.EXPECTED:
            assert expected in msg_output, f"Missing '{expected}' in messages:\n{msg_output}"

        # Plain format should also render the todo done count
        assert "done" in msg_output, f"Missing done count in messages:\n{msg_output}"


class TestChatErrorsContinue:
    """Test interactive chat with :test_errors_continue fake model.

    The :test_errors_continue model triggers 4 consecutive HTTP errors,
    exceeding the retry limit (LLM_PROVIDER_MAX_RETRIES=3). The server
    should exhaust retries and send an error event. The CLI should display
    the error gracefully and allow the user to continue the conversation.
    """

    def test_errors_continue_and_recover(self):
        """Verifies:
        - "Created conversation" status with title and ID
        - Error event is displayed after retries exhausted (4 errors > max 3 retries)
        - REPL doesn't hang or crash — accepts /quit cleanly
        """
        stdin_lines = [
            ":test_errors_continue",  # Triggers 4 errors that exhaust retries (max=3)
            "/quit",
        ]
        stdin_input = "\n".join(stdin_lines) + "\n"

        result = _run_chat(
            ["navari", "chat", "-n", "-t", "CLI Errors Continue Test"],
            stdin_input,
        )

        output = result.stdout + result.stderr

        # Verify conversation was created
        assert "Created conversation" in output, f"Missing conversation creation in:\n{output}"

        # Verify the error was displayed (server sends error event after retries exhausted)
        assert "Error:" in output, f"Missing error message in:\n{output}"

        # Verify the REPL didn't crash — it accepted /quit cleanly
        assert result.returncode == 0, f"CLI exited with code {result.returncode}:\n{output}"


class TestChatFileDownload:
    """Test /download command in interactive chat session.

    Uses :test_file_folder_basic which creates folder1/file.txt and
    folder2/file.txt via file_write + report_file_created.  Verifies
    /download within the chat and also `files list` + `files download`
    outside the chat.
    """

    # Paths the :test_file_folder_basic model creates (see tests/models/llm_file_edit.py)
    EXPECTED_PATHS = [
        "folder1/file.txt",  # relative
        "folder2/file.txt",  # relative
        "/workspace/mcp_file_edit_test/folder1/file.txt",  # absolute
        "/workspace/mcp_file_edit_test/folder2/file.txt",  # absolute
    ]

    def test_download_workspace_files(self, tmp_path):
        """Verifies:
        - :test_file_folder_basic creates workspace files in subfolders
        - /download <path> in interactive chat downloads the correct file
        - Downloaded content matches expected text (contains file path)
        - `files list <id>` shows workspace files after the session
        - `files download <path> -c <id>` works outside the chat
        """
        stdin_lines = [
            ":test_file_folder_basic",
            "/download folder1/file.txt",  # relative path
            "/download folder2/file.txt",  # relative path
            "/download /workspace/mcp_file_edit_test/folder1/file.txt",  # absolute display path
            "/download /workspace/mcp_file_edit_test/folder2/file.txt",  # absolute display path
            "/quit",
        ]
        stdin_input = "\n".join(stdin_lines) + "\n"

        result = _run_chat(
            ["navari", "chat", "-n", "-t", "CLI File Download Test"],
            stdin_input,
            cwd=str(tmp_path),  # Downloads land here
        )

        output = result.stdout + result.stderr

        if "Unknown command: test_file_folder_basic" in output:
            pytest.skip(":test_file_folder_basic model not available on this server")

        assert "Created conversation" in output, f"Missing conversation creation in:\n{output}"

        # Verify /download output shows success for both files
        assert "Downloaded" in output, f"Missing download confirmation in:\n{output}"
        assert "file.txt" in output, f"Missing file.txt in output:\n{output}"

        # Verify all 4 downloads with conflict resolution suffixes
        expected = [
            ("file.txt", "folder1/file.txt"),  # relative folder1
            ("file (1).txt", "folder2/file.txt"),  # relative folder2
            ("file (2).txt", "folder1/file.txt"),  # absolute folder1
            ("file (3).txt", "folder2/file.txt"),  # absolute folder2
        ]
        for filename, expected_content in expected:
            path = tmp_path / filename
            assert path.exists(), f"File not downloaded: {filename}"
            assert expected_content in path.read_text(), f"Wrong content in {filename}: {path.read_text()!r}"

        # --- Also test non-interactive: files list + files download ---

        # Extract conversation ID from the output
        # Output contains "Created conversation: CLI File Download Test (ID)"
        conv_line = [line for line in output.splitlines() if "Created conversation" in line]
        assert conv_line, f"Cannot find conversation ID in:\n{output}"
        conv_id = conv_line[0].split("(")[-1].rstrip(")")

        # files list shows workspace files
        result = runner.invoke(app, ["files", "list", conv_id])
        assert result.exit_code == 0
        assert "Workspace files:" in result.output
        assert "file.txt" in result.output

        # files download by relative and absolute path with -c
        for dl_path in self.EXPECTED_PATHS:
            out_name = dl_path.replace("/", "_").strip("_") + ".dl"
            result = runner.invoke(app, ["files", "-c", conv_id, "download", dl_path, "-o", str(tmp_path / out_name)])
            assert result.exit_code == 0, f"Download failed for {dl_path}: {result.output}"
            content = (tmp_path / out_name).read_text()
            assert "file.txt" in content, f"Wrong content for {dl_path}: {content!r}"
