from __future__ import annotations

import json
from pathlib import Path

import httpx
import pytest
import respx
from typer.testing import CliRunner

from simplicity_cli.cli import app

API_BASE = "https://api.simplicity.ai"
DEFAULT_ENV = {"SIMPLICITY_AI_API_KEY": "test-api-key"}


@pytest.fixture
def runner() -> CliRunner:
    return CliRunner()


@pytest.fixture(autouse=True)
def isolated_config_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))


def invoke(runner: CliRunner, args: list[str], env: dict[str, str] | None = None):
    combined_env = dict(DEFAULT_ENV)
    if env:
        combined_env.update(env)
    return runner.invoke(app, args, env=combined_env)


def task_payload(task_id: str, status: str, **extra: object) -> dict[str, object]:
    payload: dict[str, object] = {
        "task_id": task_id,
        "status": status,
        "created_at": "2025-01-01T00:00:00Z",
    }
    payload.update(extra)
    return payload


def test_auth_missing_key_fails(runner: CliRunner, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("SIMPLICITY_AI_API_KEY", raising=False)
    result = runner.invoke(app, ["status", "task-1"])
    assert result.exit_code == 2
    assert "SIMPLICITY_AI_API_KEY" in result.stdout


def test_no_args_prints_help(runner: CliRunner) -> None:
    result = runner.invoke(app, [])
    assert result.exit_code == 0
    assert "Usage:" in result.stdout


def test_login_flag_only_saves_key(runner: CliRunner, tmp_path: Path) -> None:
    xdg = tmp_path / "config-root"
    result = runner.invoke(app, ["--api-key", "saved-key-123"], env={"XDG_CONFIG_HOME": str(xdg)})
    assert result.exit_code == 0
    assert "API key saved to" in result.stdout

    config_path = xdg / "simplicity-cli" / "config.json"
    assert config_path.exists()
    payload = json.loads(config_path.read_text())
    assert payload["api_key"] == "saved-key-123"


def test_saved_key_is_used_without_env_or_flag(runner: CliRunner, tmp_path: Path) -> None:
    xdg = tmp_path / "config-root"
    login_result = runner.invoke(
        app,
        ["--api-key", "saved-key-xyz"],
        env={"XDG_CONFIG_HOME": str(xdg), "SIMPLICITY_AI_API_KEY": ""},
    )
    assert login_result.exit_code == 0

    with respx.mock(assert_all_called=True) as mock:
        def handler(request: httpx.Request) -> httpx.Response:
            assert request.headers["X-API-Key"] == "saved-key-xyz"
            return httpx.Response(200, json=task_payload("task-1", "processing"))

        mock.get(f"{API_BASE}/v1/tasks/task-1").mock(side_effect=handler)
        result = runner.invoke(
            app,
            ["status", "task-1"],
            env={"XDG_CONFIG_HOME": str(xdg), "SIMPLICITY_AI_API_KEY": ""},
        )
    assert result.exit_code == 0


def test_api_key_flag_overrides_env(runner: CliRunner) -> None:
    with respx.mock(assert_all_called=True) as mock:
        def handler(request: httpx.Request) -> httpx.Response:
            assert request.headers["X-API-Key"] == "flag-key"
            return httpx.Response(200, json=task_payload("task-1", "processing"))

        mock.get(f"{API_BASE}/v1/tasks/task-1").mock(side_effect=handler)
        result = invoke(
            runner,
            ["--api-key", "flag-key", "status", "task-1"],
            env={"SIMPLICITY_AI_API_KEY": "env-key"},
        )
    assert result.exit_code == 0


def test_top_level_help_uses_flat_commands(runner: CliRunner) -> None:
    result = invoke(runner, ["--help"])
    assert result.exit_code == 0
    assert "new" in result.stdout
    assert "existing" in result.stdout
    assert "status" in result.stdout
    assert "wait" in result.stdout
    assert "│ fill" not in result.stdout
    assert "│ task" not in result.stdout


def test_legacy_nested_alias_still_supported(runner: CliRunner) -> None:
    with respx.mock(assert_all_called=True) as mock:
        mock.get(f"{API_BASE}/v1/tasks/task-legacy").respond(200, json=task_payload("task-legacy", "processing"))
        result = invoke(runner, ["task", "status", "task-legacy"])
    assert result.exit_code == 0


def test_one_page_help_command(runner: CliRunner) -> None:
    result = invoke(runner, ["help"])
    assert result.exit_code == 0
    assert "one-page help" in result.stdout
    assert "simplicity-cli new" in result.stdout
    assert "simplicity-cli existing" in result.stdout
    assert "simplicity-cli status" in result.stdout
    assert "simplicity-cli wait" in result.stdout


def test_fill_new_rejects_invalid_form_input_combination(runner: CliRunner, tmp_path: Path) -> None:
    form_file = tmp_path / "form.pdf"
    form_file.write_bytes(b"form")
    result = invoke(
        runner,
        [
            "fill",
            "new",
            "--form-file",
            str(form_file),
            "--form-url",
            "https://example.com/form.pdf",
            "--context",
            "hello",
        ],
    )
    assert result.exit_code == 2
    assert "exactly one of --form-file or --form-url" in result.stdout


def test_fill_new_requires_source_or_context(runner: CliRunner, tmp_path: Path) -> None:
    form_file = tmp_path / "form.pdf"
    form_file.write_bytes(b"form")
    result = invoke(runner, ["new", "--form-file", str(form_file)])
    assert result.exit_code == 2
    assert "requires at least one source input" in result.stdout


def test_fill_new_local_files_downloads_default_output(
    runner: CliRunner,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.chdir(tmp_path)
    form_file = tmp_path / "form.pdf"
    source_file = tmp_path / "source.pdf"
    form_file.write_bytes(b"form")
    source_file.write_bytes(b"source")

    with respx.mock(assert_all_called=True) as mock:
        mock.post(f"{API_BASE}/v1/documents/upload-document").mock(
            side_effect=[
                httpx.Response(200, json={"task_id": "upload-form-task"}),
                httpx.Response(200, json={"task_id": "upload-source-task"}),
            ]
        )
        mock.get(f"{API_BASE}/v1/tasks/upload-form-task").respond(
            200,
            json=task_payload("upload-form-task", "completed", document_id="form-doc"),
        )
        mock.get(f"{API_BASE}/v1/tasks/upload-source-task").respond(
            200,
            json=task_payload("upload-source-task", "completed", document_id="source-doc"),
        )
        mock.post(f"{API_BASE}/v1/forms/autofill").respond(
            200,
            json={"task_id": "autofill-task", "status": "processing"},
        )
        mock.get(f"{API_BASE}/v1/tasks/autofill-task").respond(
            200,
            json=task_payload("autofill-task", "completed", result_data={"form_id": "form-123"}),
        )
        mock.get(f"{API_BASE}/v1/forms/form-123").respond(
            200,
            json={
                "form_id": "form-123",
                "source_document_details": [],
                "form_url": "https://download.test/form-123.pdf",
            },
        )
        mock.get("https://download.test/form-123.pdf").respond(200, content=b"filled")
        result = invoke(
            runner,
            [
                "fill",
                "new",
                "--form-file",
                str(form_file),
                "--source-file",
                str(source_file),
            ],
        )

    assert result.exit_code == 0
    output_file = tmp_path / "form-123.filled.pdf"
    assert output_file.exists()
    assert output_file.read_bytes() == b"filled"


def test_fill_new_with_url_uploads(runner: CliRunner) -> None:
    with respx.mock(assert_all_called=True) as mock:
        mock.post(f"{API_BASE}/v1/documents/upload-document-url").mock(
            side_effect=[
                httpx.Response(200, json={"task_id": "upload-form-task"}),
                httpx.Response(200, json={"task_id": "upload-source-task"}),
            ]
        )
        mock.get(f"{API_BASE}/v1/tasks/upload-form-task").respond(
            200,
            json=task_payload("upload-form-task", "completed", document_id="form-doc"),
        )
        mock.get(f"{API_BASE}/v1/tasks/upload-source-task").respond(
            200,
            json=task_payload("upload-source-task", "completed", document_id="source-doc"),
        )
        mock.post(f"{API_BASE}/v1/forms/autofill").respond(
            200,
            json={"task_id": "autofill-task", "status": "processing"},
        )
        mock.get(f"{API_BASE}/v1/tasks/autofill-task").respond(
            200,
            json=task_payload("autofill-task", "completed", result_data={"form_id": "form-200"}),
        )
        result = invoke(
            runner,
            [
                "fill",
                "new",
                "--form-url",
                "https://example.com/form.pdf",
                "--source-url",
                "https://example.com/source.pdf",
                "--no-download",
            ],
        )
    assert result.exit_code == 0
    assert "task_id: autofill-task" in result.stdout


def test_fill_new_no_wait_does_not_poll_autofill(runner: CliRunner, tmp_path: Path) -> None:
    form_file = tmp_path / "form.pdf"
    form_file.write_bytes(b"form")
    with respx.mock(assert_all_called=False) as mock:
        mock.post(f"{API_BASE}/v1/documents/upload-document").respond(200, json={"task_id": "upload-form-task"})
        mock.get(f"{API_BASE}/v1/tasks/upload-form-task").respond(
            200,
            json=task_payload("upload-form-task", "completed", document_id="form-doc"),
        )
        mock.post(f"{API_BASE}/v1/forms/autofill").respond(
            200,
            json={"task_id": "autofill-task", "status": "processing"},
        )
        autofill_poll = mock.get(f"{API_BASE}/v1/tasks/autofill-task").respond(
            200,
            json=task_payload("autofill-task", "completed", result_data={"form_id": "form-201"}),
        )
        result = invoke(
            runner,
            [
                "fill",
                "new",
                "--form-file",
                str(form_file),
                "--context",
                "hello world",
                "--no-wait",
                "--no-download",
            ],
        )
    assert result.exit_code == 0
    assert not autofill_poll.called


def test_fill_existing_wait_downloads_by_form_id(
    runner: CliRunner,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.chdir(tmp_path)
    with respx.mock(assert_all_called=True) as mock:
        mock.post(f"{API_BASE}/v1/forms/form-9/autofill").respond(
            200,
            json={"task_id": "autofill-task", "status": "processing"},
        )
        mock.get(f"{API_BASE}/v1/tasks/autofill-task").respond(
            200,
            json=task_payload("autofill-task", "completed"),
        )
        mock.get(f"{API_BASE}/v1/forms/form-9").respond(
            200,
            json={
                "form_id": "form-9",
                "source_document_details": [],
                "form_url": "https://download.test/form-9.pdf",
            },
        )
        mock.get("https://download.test/form-9.pdf").respond(200, content=b"filled-existing")
        result = invoke(runner, ["fill", "existing", "form-9", "--context", "Use existing info"])
    assert result.exit_code == 0
    assert (tmp_path / "form-9.filled.pdf").read_bytes() == b"filled-existing"


def test_task_wait_polls_until_completed(runner: CliRunner) -> None:
    with respx.mock(assert_all_called=True) as mock:
        poll_route = mock.get(f"{API_BASE}/v1/tasks/task-7").mock(
            side_effect=[
                httpx.Response(200, json=task_payload("task-7", "pending")),
                httpx.Response(200, json=task_payload("task-7", "processing")),
                httpx.Response(200, json=task_payload("task-7", "completed")),
            ]
        )
        result = invoke(
            runner,
            [
                "task",
                "wait",
                "task-7",
                "--poll-interval-seconds",
                "0.01",
                "--max-wait-seconds",
                "3",
            ],
        )
    assert result.exit_code == 0
    assert poll_route.call_count == 3


def test_task_wait_handles_failed_task(runner: CliRunner) -> None:
    with respx.mock(assert_all_called=True) as mock:
        mock.get(f"{API_BASE}/v1/tasks/task-failed").respond(
            200,
            json=task_payload("task-failed", "failed", error_message="autofill exploded"),
        )
        result = invoke(runner, ["task", "wait", "task-failed", "--poll-interval-seconds", "0.01"])
    assert result.exit_code == 5
    assert "autofill exploded" in result.stdout


def test_task_wait_timeout_returns_exit_code_6(runner: CliRunner) -> None:
    with respx.mock(assert_all_called=True) as mock:
        mock.get(f"{API_BASE}/v1/tasks/task-timeout").respond(200, json=task_payload("task-timeout", "processing"))
        result = invoke(
            runner,
            [
                "task",
                "wait",
                "task-timeout",
                "--poll-interval-seconds",
                "0.01",
                "--max-wait-seconds",
                "0.05",
            ],
        )
    assert result.exit_code == 6
    assert "did not complete" in result.stdout


def test_download_falls_back_to_document_presigned_url(
    runner: CliRunner,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.chdir(tmp_path)
    form_file = tmp_path / "form.pdf"
    form_file.write_bytes(b"form")

    with respx.mock(assert_all_called=True) as mock:
        mock.post(f"{API_BASE}/v1/documents/upload-document").respond(200, json={"task_id": "upload-form-task"})
        mock.get(f"{API_BASE}/v1/tasks/upload-form-task").respond(
            200,
            json=task_payload("upload-form-task", "completed", document_id="form-doc"),
        )
        mock.post(f"{API_BASE}/v1/forms/autofill").respond(
            200,
            json={"task_id": "autofill-task", "status": "processing"},
        )
        mock.get(f"{API_BASE}/v1/tasks/autofill-task").respond(
            200,
            json=task_payload("autofill-task", "completed", result_data={"document_id": "filled-doc-1"}),
        )
        mock.get(f"{API_BASE}/v1/documents/filled-doc-1").respond(
            200,
            json={
                "document_id": "filled-doc-1",
                "file_key": "file-key",
                "presigned_url": "https://download.test/filled-doc-1.pdf",
            },
        )
        mock.get("https://download.test/filled-doc-1.pdf").respond(200, content=b"filled-doc")
        result = invoke(
            runner,
            [
                "fill",
                "new",
                "--form-file",
                str(form_file),
                "--context",
                "fallback to doc URL",
            ],
        )
    assert result.exit_code == 0
    assert (tmp_path / "filled-doc-1.filled.pdf").read_bytes() == b"filled-doc"


def test_json_output_is_single_object(runner: CliRunner) -> None:
    with respx.mock(assert_all_called=True) as mock:
        mock.get(f"{API_BASE}/v1/tasks/task-json").respond(200, json=task_payload("task-json", "processing"))
        result = invoke(runner, ["--json", "status", "task-json"])
    assert result.exit_code == 0
    lines = [line for line in result.stdout.splitlines() if line.strip()]
    assert len(lines) == 1
    payload = json.loads(lines[0])
    assert payload["command"] == "task status"
    assert payload["ok"] is True
    assert payload["error"] is None


@pytest.mark.parametrize("json_mode", [False, True])
def test_http_422_surfaces_validation_details(runner: CliRunner, json_mode: bool) -> None:
    with respx.mock(assert_all_called=True) as mock:
        mock.get(f"{API_BASE}/v1/tasks/task-bad").respond(
            422,
            json={"detail": [{"loc": ["query", "x"], "msg": "Invalid page", "type": "value_error"}]},
        )
        args = ["task", "status", "task-bad"]
        if json_mode:
            args.insert(0, "--json")
        result = invoke(runner, args)
    assert result.exit_code == 4
    if json_mode:
        payload = json.loads(result.stdout.strip())
        assert payload["error"]["code"] == "validation_error"
        assert payload["error"]["details"]["status_code"] == 422
    else:
        assert "Invalid page" in result.stdout
