from __future__ import annotations

import importlib
import json
import os
import sys

import click
import pytest

from cli.config import CONFIG_DIR_ENV, CLIConfig, ConfigStore

try:
    from shared import (
        AuthenticationError,
        BackendRequestError,
        ConflictError,
        NotFoundError,
        PreconditionFailedError,
        ValidationFailure,
    )
except ImportError:
    from cli._vendor.shared import (
        AuthenticationError,
        BackendRequestError,
        ConflictError,
        NotFoundError,
        PreconditionFailedError,
        ValidationFailure,
    )


def _reload_cli_module(monkeypatch: pytest.MonkeyPatch, tmp_path) -> object:
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    import cli.main as cli_main

    return importlib.reload(cli_main)


def _set_argv(monkeypatch: pytest.MonkeyPatch, args: list[str]) -> None:
    monkeypatch.setattr(sys, "argv", ["ctxme", *args])


def test_cli_error_outputs_clean_message(tmp_path, monkeypatch, capsys) -> None:
    cli_main = _reload_cli_module(monkeypatch, tmp_path)
    monkeypatch.delenv("CTXME_DEBUG", raising=False)
    _set_argv(monkeypatch, ["list"])

    with pytest.raises(SystemExit) as excinfo:
        cli_main.main()

    assert excinfo.value.code == 1
    captured = capsys.readouterr()
    assert "Error:" in captured.err
    # Auth error surfaces before project error per issue #190
    assert "Not signed in" in captured.err
    assert "Traceback" not in captured.err
    assert captured.out == ""


def test_debug_env_prints_traceback(tmp_path, monkeypatch, capsys) -> None:
    cli_main = _reload_cli_module(monkeypatch, tmp_path)
    monkeypatch.setenv("CTXME_DEBUG", "true")
    _set_argv(monkeypatch, ["list"])

    with pytest.raises(SystemExit):
        cli_main.main()

    captured = capsys.readouterr()
    assert "Traceback" in captured.err


def test_debug_flag_enables_traceback(tmp_path, monkeypatch, capsys) -> None:
    cli_main = _reload_cli_module(monkeypatch, tmp_path)
    monkeypatch.delenv("CTXME_DEBUG", raising=False)
    _set_argv(monkeypatch, ["--debug", "list"])

    with pytest.raises(SystemExit):
        cli_main.main()

    captured = capsys.readouterr()
    assert os.getenv("CTXME_DEBUG") == "1"
    assert "Traceback" in captured.err


def test_not_found_maps_to_user_message(tmp_path, monkeypatch, capsys) -> None:
    cli_main = _reload_cli_module(monkeypatch, tmp_path)
    monkeypatch.delenv("CTXME_DEBUG", raising=False)

    store = ConfigStore()
    store.save(CLIConfig(default_project="missing", use_mock_client=True))

    _set_argv(monkeypatch, ["list"])
    with pytest.raises(SystemExit):
        cli_main.main()

    captured = capsys.readouterr()
    assert "Item/Project not found:" in captured.err
    assert "missing" in captured.err


def test_debug_shared_error_includes_request_id(tmp_path, monkeypatch, capsys) -> None:
    cli_main = _reload_cli_module(monkeypatch, tmp_path)
    monkeypatch.setenv("CTXME_DEBUG", "1")

    error = BackendRequestError(
        "boom",
        status_code=502,
        request_id="req-123",
        response_body="oops",
        request_url="http://example.test/v1/items",
    )

    def _fail(*_args: object, **_kwargs: object) -> None:
        raise error

    monkeypatch.setattr(cli_main, "app", _fail)

    with pytest.raises(SystemExit):
        cli_main.main()

    captured = capsys.readouterr()
    assert "API error: boom" in captured.err
    assert "Request ID:" in captured.err
    assert "req-123" in captured.err


def test_missing_command_auth_shows_help(tmp_path, monkeypatch, capsys) -> None:
    """Running 'ctxme auth' without subcommand should show error and help."""
    cli_main = _reload_cli_module(monkeypatch, tmp_path)
    _set_argv(monkeypatch, ["auth"])

    with pytest.raises(SystemExit) as excinfo:
        cli_main.main()

    assert excinfo.value.code == 2
    captured = capsys.readouterr()
    # Error message should be present
    assert "Missing command" in captured.err
    # Help text should be appended
    assert "login" in captured.err
    assert "logout" in captured.err
    assert "status" in captured.err
    # Nothing on stdout
    assert captured.out == ""


def test_missing_command_projects_shows_help(tmp_path, monkeypatch, capsys) -> None:
    """Running 'ctxme projects' without subcommand should show error and help."""
    cli_main = _reload_cli_module(monkeypatch, tmp_path)
    _set_argv(monkeypatch, ["projects"])

    with pytest.raises(SystemExit) as excinfo:
        cli_main.main()

    assert excinfo.value.code == 2
    captured = capsys.readouterr()
    # Error message should be present
    assert "Missing command" in captured.err
    # Help text should be appended
    assert "list" in captured.err
    assert "create" in captured.err
    assert "delete" in captured.err
    # Nothing on stdout
    assert captured.out == ""


def test_unknown_command_no_help_appended(tmp_path, monkeypatch, capsys) -> None:
    """Unknown command should show error but NOT append full help."""
    cli_main = _reload_cli_module(monkeypatch, tmp_path)
    _set_argv(monkeypatch, ["auth", "unknown-cmd"])

    with pytest.raises(SystemExit) as excinfo:
        cli_main.main()

    assert excinfo.value.code == 2
    captured = capsys.readouterr()
    # Error should mention unknown command
    assert "No such command" in captured.err
    # But full help listing should NOT be appended (login/logout are subcommands)
    # The error line mentions 'auth --help' hint, but not the full command list
    lines = captured.err.strip().split("\n")
    # Should be short - just usage line, hint, and error
    assert len(lines) < 10


# =============================================================================
# JSON Error Output Tests (Issue #260)
# =============================================================================


class TestJSONErrorOutput:
    """Tests for structured JSON error output when --output json is used."""

    def test_json_error_has_required_fields(self, tmp_path, monkeypatch, capsys) -> None:
        """JSON errors must include 'error' and 'message' fields."""
        cli_main = _reload_cli_module(monkeypatch, tmp_path)
        monkeypatch.delenv("CTXME_DEBUG", raising=False)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()

        # JSON should be on stdout, not stderr
        assert captured.out.strip() != ""
        error_json = json.loads(captured.out)

        # Required fields
        assert "error" in error_json
        assert "message" in error_json
        assert isinstance(error_json["error"], str)
        assert isinstance(error_json["message"], str)

    def test_json_error_valid_json(self, tmp_path, monkeypatch, capsys) -> None:
        """JSON error output must be valid, parseable JSON."""
        cli_main = _reload_cli_module(monkeypatch, tmp_path)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        # Should not raise json.JSONDecodeError
        error_json = json.loads(captured.out)
        assert isinstance(error_json, dict)

    def test_json_error_no_styled_output(self, tmp_path, monkeypatch, capsys) -> None:
        """JSON mode should suppress Rich styled output."""
        cli_main = _reload_cli_module(monkeypatch, tmp_path)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        # No Rich formatting markers in output
        assert "[red]" not in captured.out
        assert "[/red]" not in captured.out
        # stderr should be empty (no styled error messages)
        assert captured.err == ""

    def test_authentication_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """AuthenticationError should map to 'authentication_error' error code."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        error = AuthenticationError(
            "Invalid API key",
            status_code=401,
            request_id="req-auth-123",
            response_body=None,
            request_url="http://example.test/v1/items",
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        # Set JSON mode directly since app callback won't run
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "authentication_error"
        assert "hint" in error_json
        assert "ctx auth login" in error_json["hint"]
        assert error_json["request_id"] == "req-auth-123"
        assert error_json["status_code"] == 401

    def test_not_found_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """NotFoundError should map to 'not_found' error code."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        error = NotFoundError(
            "Item 'my-item' not found in project 'my-project'",
            status_code=404,
            request_id="req-404",
            response_body=None,
            request_url="http://example.test/v1/items/my-item",
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "not_found"
        assert "my-item" in error_json["message"]
        assert error_json["status_code"] == 404
        assert error_json["request_id"] == "req-404"

    def test_validation_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """ValidationFailure should map to 'validation_error' error code."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        error = ValidationFailure(
            "Invalid item key: must be lowercase",
            status_code=422,
            request_id="req-validation",
            response_body=None,
            request_url="http://example.test/v1/items",
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "validation_error"
        assert error_json["status_code"] == 422

    def test_conflict_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """ConflictError should map to 'conflict_error' error code."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        error = ConflictError(
            "Item 'my-item' already exists",
            status_code=409,
            request_id="req-conflict",
            response_body=None,
            request_url="http://example.test/v1/items",
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "conflict_error"
        assert error_json["status_code"] == 409

    def test_precondition_failed_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """PreconditionFailedError should map to 'precondition_failed' error code."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        error = PreconditionFailedError(
            "ETag mismatch: resource was modified",
            status_code=412,
            request_id="req-etag",
            response_body=None,
            request_url="http://example.test/v1/items/my-item",
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "precondition_failed"
        assert error_json["status_code"] == 412
        assert "hint" in error_json
        assert "modified" in error_json["hint"].lower()

    def test_usage_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """click.UsageError should map to 'usage_error' error code."""
        cli_main = _reload_cli_module(monkeypatch, tmp_path)
        _set_argv(monkeypatch, ["--output", "json", "auth", "unknown-cmd"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 2
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "usage_error"
        assert "hint" in error_json
        assert "--help" in error_json["hint"]

    def test_abort_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """click.Abort should emit JSON with error: 'aborted'."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _abort(*_args: object, **_kwargs: object) -> None:
            raise click.Abort()

        monkeypatch.setattr(cli_main, "app", _abort)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "aborted"
        # No "Aborted!" text on stderr in JSON mode
        assert "Aborted!" not in captured.err

    def test_unexpected_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """Unexpected exceptions should map to 'unexpected_error' error code."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise RuntimeError("Something went wrong")

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "unexpected_error"
        assert "hint" in error_json
        assert "CTXME_DEBUG" in error_json["hint"]

    def test_status_code_is_integer(self, tmp_path, monkeypatch, capsys) -> None:
        """status_code field must be an integer, not a string."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        error = NotFoundError(
            "Not found",
            status_code=404,
            request_id=None,
            response_body=None,
            request_url=None,
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert isinstance(error_json["status_code"], int)
        assert error_json["status_code"] == 404

    def test_response_body_not_included(self, tmp_path, monkeypatch, capsys) -> None:
        """response_body should NOT be included in JSON output (security)."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        error = BackendRequestError(
            "Server error",
            status_code=500,
            request_id="req-500",
            response_body='{"secret": "sensitive_data"}',
            request_url="http://example.test/v1/items",
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        # response_body must not be included
        assert "response_body" not in error_json
        assert "sensitive_data" not in captured.out

    def test_keyboard_interrupt_exits_silently(self, tmp_path, monkeypatch, capsys) -> None:
        """Ctrl+C (KeyboardInterrupt) should exit with code 130, no JSON output."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _interrupt(*_args: object, **_kwargs: object) -> None:
            raise KeyboardInterrupt

        monkeypatch.setattr(cli_main, "app", _interrupt)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 130
        captured = capsys.readouterr()
        # No output at all - silent exit
        assert captured.out == ""
        assert captured.err == ""

    def test_exit_codes_preserved_in_json_mode(self, tmp_path, monkeypatch, capsys) -> None:
        """Exit codes should remain consistent in JSON mode."""
        cli_main = _reload_cli_module(monkeypatch, tmp_path)
        _set_argv(monkeypatch, ["--output", "json", "auth", "unknown-cmd"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        # UsageError should still exit with code 2
        assert excinfo.value.code == 2

    def test_json_output_to_stdout(self, tmp_path, monkeypatch, capsys) -> None:
        """JSON errors should go to stdout, not stderr."""
        cli_main = _reload_cli_module(monkeypatch, tmp_path)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        # JSON should be on stdout
        assert captured.out.strip() != ""
        json.loads(captured.out)  # Should not raise
        # stderr should be empty
        assert captured.err == ""

    def test_configuration_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """ConfigurationError should map to 'configuration_error' error code."""
        from cli.exceptions import ConfigurationError
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise ConfigurationError("Config file is invalid")

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "configuration_error"

    def test_keyring_failure_json(self, tmp_path, monkeypatch, capsys) -> None:
        """KeyringFailure should map to 'keyring_error' error code."""
        from cli.exceptions import KeyringFailure
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise KeyringFailure("Keychain access denied")

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "keyring_error"

    def test_cli_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """CLIError (base) should map to 'cli_error' error code."""
        from cli.exceptions import CLIError
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise CLIError("Something went wrong in CLI")

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "cli_error"

    def test_backend_request_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """BackendRequestError (generic) should map to 'backend_error' error code."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        # Use base BackendRequestError, not a subclass
        error = BackendRequestError(
            "Generic backend error",
            status_code=500,
            request_id="req-backend",
            response_body=None,
            request_url="http://example.test/v1/items",
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "backend_error"
        assert error_json["status_code"] == 500

    def test_config_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """ConfigError (shared) should map to 'config_error' error code."""
        from cli.options import OutputMode

        try:
            from shared import ConfigError
        except ImportError:
            from cli._vendor.shared import ConfigError

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise ConfigError("Shared config error")

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "config_error"
        assert "hint" in error_json
        assert "ctx config show" in error_json["hint"]

    def test_connect_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """httpx.ConnectError should map to 'connection_error' error code."""
        import httpx

        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise httpx.ConnectError("Connection refused")

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "connection_error"
        assert "hint" in error_json
        assert "network connection" in error_json["hint"].lower()

    def test_timeout_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """httpx.TimeoutException should map to 'timeout_error' error code."""
        import httpx

        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise httpx.ReadTimeout("Request timed out")

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "timeout_error"

    def test_http_status_error_json(self, tmp_path, monkeypatch, capsys) -> None:
        """httpx.HTTPStatusError should map to 'http_error' error code."""
        import httpx

        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        # Create a mock response for HTTPStatusError
        request = httpx.Request("GET", "http://example.test/v1/items")
        response = httpx.Response(502, request=request)

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise httpx.HTTPStatusError("Bad Gateway", request=request, response=response)

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "http_error"
        assert error_json["status_code"] == 502

    def test_click_exception_json(self, tmp_path, monkeypatch, capsys) -> None:
        """click.ClickException should map to 'click_error' error code."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise click.ClickException("Click exception occurred")

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit) as excinfo:
            cli_main.main()

        assert excinfo.value.code == 1
        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "click_error"


# =============================================================================
# Integration Tests (Issue #260)
# =============================================================================


class TestJSONErrorIntegration:
    """Integration tests that verify JSON error output via subprocess."""

    def test_json_error_parseable_by_jq(self, tmp_path, monkeypatch) -> None:
        """Integration test: JSON output should be parseable by jq.

        Verifies: ctx items get nonexistent --output json | jq .error
        returns expected error code.
        """
        import subprocess

        # Set up test config with mock client and a project
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        store = ConfigStore()
        store.save(CLIConfig(default_project="test-project", use_mock_client=True))

        # Run CLI with --output json for a nonexistent item
        result = subprocess.run(  # noqa: S603 - test uses trusted input
            [
                sys.executable,
                "-m",
                "cli.main",
                "--output",
                "json",
                "get",
                "nonexistent-item-key",
            ],
            capture_output=True,
            text=True,
            env={**os.environ, CONFIG_DIR_ENV: str(tmp_path)},
        )

        # Should exit with non-zero code
        assert result.returncode != 0

        # stdout should contain valid JSON
        assert result.stdout.strip() != ""
        error_json = json.loads(result.stdout)

        # Should have error field
        assert "error" in error_json
        assert error_json["error"] == "not_found"

        # stderr should be empty (no Rich formatting leaked)
        assert result.stderr == ""

    def test_json_error_cli_error_integration(self, tmp_path, monkeypatch) -> None:
        """Integration test: CLI errors emit valid JSON."""
        import subprocess

        # Set up test config WITHOUT default project to trigger config error
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        store = ConfigStore()
        # Save config without default_project and without mock client
        store.save(CLIConfig(default_project=None, use_mock_client=False))

        # Run CLI with --output json - should fail with config error
        result = subprocess.run(  # noqa: S603 - test uses trusted input
            [
                sys.executable,
                "-m",
                "cli.main",
                "--output",
                "json",
                "list",
            ],
            capture_output=True,
            text=True,
            env={**os.environ, CONFIG_DIR_ENV: str(tmp_path)},
        )

        # Should exit with non-zero code
        assert result.returncode != 0

        # stdout should contain valid JSON
        assert result.stdout.strip() != ""
        error_json = json.loads(result.stdout)

        # Should have required fields
        assert "error" in error_json
        assert "message" in error_json

    def test_json_error_usage_error_integration(self, tmp_path, monkeypatch) -> None:
        """Integration test: Usage errors emit valid JSON."""
        import subprocess

        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))

        # Run CLI with invalid command
        result = subprocess.run(  # noqa: S603 - test uses trusted input
            [
                sys.executable,
                "-m",
                "cli.main",
                "--output",
                "json",
                "nonexistent-command",
            ],
            capture_output=True,
            text=True,
            env={**os.environ, CONFIG_DIR_ENV: str(tmp_path)},
        )

        # Should exit with code 2 (usage error)
        assert result.returncode == 2

        # stdout should contain valid JSON
        assert result.stdout.strip() != ""
        error_json = json.loads(result.stdout)

        assert error_json["error"] == "usage_error"


# =============================================================================
# NotFoundError Context Capture Tests (Issue #260 - explicit context)
# =============================================================================


class TestNotFoundResourceContext:
    """Tests for resource_type context capture in NotFoundError (issue #260).

    These tests verify that the CLI uses explicit resource_type/resource_key
    context captured at raise sites instead of parsing error messages.
    """

    def test_project_not_found_hint_from_resource_type(self, tmp_path, monkeypatch, capsys) -> None:
        """NotFoundError with resource_type='project' shows project-specific hint."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        # Create error with explicit resource_type context
        error = NotFoundError(
            "Project 'my-project' not found.",
            resource_type="project",
            resource_key="my-project",
            status_code=404,
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "not_found"
        assert "ctx projects list" in error_json["hint"]
        # Should include resource context in context dict
        assert error_json.get("context", {}).get("resource_type") == "project"
        assert error_json.get("context", {}).get("resource_key") == "my-project"

    def test_item_not_found_hint_from_resource_type(self, tmp_path, monkeypatch, capsys) -> None:
        """NotFoundError with resource_type='item' shows item-specific hint."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        # Create error with explicit resource_type context
        error = NotFoundError(
            "Item 'my-project/my-item' not found.",
            resource_type="item",
            resource_key="my-project/my-item",
            status_code=404,
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "not_found"
        assert "ctx items list" in error_json["hint"]
        assert error_json.get("context", {}).get("resource_type") == "item"
        assert error_json.get("context", {}).get("resource_key") == "my-project/my-item"

    def test_file_not_found_hint_from_resource_type(self, tmp_path, monkeypatch, capsys) -> None:
        """NotFoundError with resource_type='file' shows item-specific hint."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        error = NotFoundError(
            "File 'my-project/file-123' not found.",
            resource_type="file",
            resource_key="my-project/file-123",
            status_code=404,
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "not_found"
        # Files use item hint since they're in projects
        assert "ctx items list" in error_json["hint"]
        assert error_json.get("context", {}).get("resource_type") == "file"

    def test_not_found_fallback_without_resource_type(self, tmp_path, monkeypatch, capsys) -> None:
        """NotFoundError without resource_type uses generic hint."""
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        # HTTP 404 without resource_type context
        error = NotFoundError(
            "Resource not found",
            status_code=404,
            request_id="req-http-404",
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "not_found"
        # Generic hint when no resource_type
        assert "ctx projects list" in error_json["hint"] or "ctx items list" in error_json["hint"]
        # No context dict when no resource info
        assert error_json.get("context", {}).get("resource_type") is None

    def test_project_key_with_project_substring_uses_item_hint(
        self, tmp_path, monkeypatch, capsys
    ) -> None:
        """Item in project with 'project' in name should show item hint, not project hint.

        This is the edge case that issue #260 addressed - parsing "project" from
        the message would give wrong hint when project_key contains "project".
        """
        from cli.options import OutputMode

        cli_main = _reload_cli_module(monkeypatch, tmp_path)

        # Item in a project named "my-project-data" - has "project" in key
        error = NotFoundError(
            "Item 'my-project-data/my-item' not found.",
            resource_type="item",  # Explicit context says it's an item
            resource_key="my-project-data/my-item",
            status_code=404,
        )

        def _fail(*_args: object, **_kwargs: object) -> None:
            raise error

        monkeypatch.setattr(cli_main, "app", _fail)
        monkeypatch.setattr(cli_main, "_output_mode", OutputMode.JSON)
        _set_argv(monkeypatch, ["--output", "json", "list"])

        with pytest.raises(SystemExit):
            cli_main.main()

        captured = capsys.readouterr()
        error_json = json.loads(captured.out)

        assert error_json["error"] == "not_found"
        # Should show item hint because resource_type='item', not project hint
        # even though "project" appears in the message
        assert "ctx items list" in error_json["hint"]
        assert "ctx projects list" not in error_json["hint"]
