from __future__ import annotations

import importlib

import pytest
from typer.testing import CliRunner

from cli.config import CONFIG_DIR_ENV, CLIConfig, ConfigStore
from cli.exceptions import ConfigurationError


def _reload_cli_module():
    import cli.main as cli_main

    return importlib.reload(cli_main)


def test_logout_clears_default_project(tmp_path, monkeypatch, in_memory_keyring):
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(default_project="core"))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["auth", "logout"])

    assert result.exit_code == 0
    config = ConfigStore().load()
    assert config.default_project is None

    result = runner.invoke(cli_main.app, ["projects", "current"])
    assert result.exit_code == 0
    assert "No default project set" in result.stdout


def test_ls_command_uses_mock_backend(tmp_path, monkeypatch):
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

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

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["ls"])

    assert result.exit_code == 0
    assert "welcome" in result.stdout


def test_list_command_alias_matches_ls(tmp_path, monkeypatch):
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

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

    runner = CliRunner()
    result_ls = runner.invoke(cli_main.app, ["ls"])
    result_list = runner.invoke(cli_main.app, ["list"])

    assert result_ls.exit_code == 0
    assert result_list.exit_code == 0
    assert result_ls.stdout == result_list.stdout


def test_list_all_flag_lists_across_projects(tmp_path, monkeypatch):
    """Test that --all flag lists items across all projects."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["ls", "--all"])

    assert result.exit_code == 0
    assert "Items across all projects" in result.stdout
    # Both projects should appear
    assert "core" in result.stdout
    assert "engineering" in result.stdout
    assert "welcome" in result.stdout
    assert "runbook" in result.stdout


def test_list_all_flag_returns_results_from_multiple_projects(tmp_path, monkeypatch):
    """Test that --all returns results from multiple projects."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["ls", "--all", "--json"])

    assert result.exit_code == 0
    import json

    data = json.loads(result.stdout)
    assert "items" in data
    # Verify items from BOTH projects appear
    project_keys = {item["project_key"] for item in data["items"]}
    assert "core" in project_keys
    assert "engineering" in project_keys


def test_list_all_and_project_flags_mutually_exclusive(tmp_path, monkeypatch):
    """Test that --all and --project cannot be used together."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["ls", "--all", "--project", "core"])

    assert result.exit_code == 1
    assert "Cannot use --all with --project" in str(result.exception)


def test_list_all_with_limit_caps_results(tmp_path, monkeypatch):
    """Test that --limit caps the number of results returned with --all."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()

    # Without limit, should have both items
    result_unlimited = runner.invoke(cli_main.app, ["ls", "--all", "--json"])
    assert result_unlimited.exit_code == 0
    import json

    data_unlimited = json.loads(result_unlimited.stdout)
    assert len(data_unlimited["items"]) == 2

    # With limit=1, should only have 1 item
    result_limited = runner.invoke(cli_main.app, ["ls", "--all", "--limit", "1", "--json"])
    assert result_limited.exit_code == 0
    data_limited = json.loads(result_limited.stdout)
    assert len(data_limited["items"]) == 1


def test_list_errors_when_no_project_and_no_default(tmp_path, monkeypatch):
    """Test that list errors when no --project, no --all, and no default project."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))  # No default_project

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["ls"])

    assert result.exit_code != 0
    # Should show error instructing user to specify --project or --all
    assert "No project specified" in result.stdout or "No project specified" in str(
        result.exception
    )
    assert "--project" in result.stdout or "--project" in str(result.exception)
    assert "--all" in result.stdout or "--all" in str(result.exception)


def test_list_with_default_project_lists_only_that_project(tmp_path, monkeypatch):
    """Test that list uses default project when configured."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

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

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["ls"])

    assert result.exit_code == 0
    assert "Items in core" in result.stdout
    # Should NOT list all projects or show info message
    assert "across all projects" not in result.stdout
    assert "no default project set" not in result.stdout


def test_list_all_short_flag(tmp_path, monkeypatch):
    """Test that -a short flag works for --all."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["ls", "-a"])

    assert result.exit_code == 0
    assert "Items across all projects" in result.stdout


@pytest.mark.parametrize(
    "args",
    [
        ["ls"],
        ["get", "welcome", "--stdout"],
        ["read", "welcome", "--stdout"],
        ["put", "welcome", "--data", "payload"],
        ["update", "welcome", "--title", "Updated title"],
        ["remove", "welcome", "--force"],
        ["search", "welcome"],
    ],
)
def test_item_commands_require_auth(tmp_path, monkeypatch, in_memory_keyring, args):
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()
    cli_main._context.config_store = ConfigStore(base_dir=tmp_path)

    store = cli_main._context.config_store
    store.save(CLIConfig(api_base_url="http://context.local", default_project="core"))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, args)

    assert result.exit_code == 1
    assert "Not signed in. Run 'ctxme auth login' to authenticate." in str(result.exception)


def test_item_commands_auth_error_precedes_config_error(tmp_path, monkeypatch, in_memory_keyring):
    """Auth errors should surface before client configuration errors (issue #190)."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()
    config = CLIConfig(default_project="core")  # No mock mode, no API key
    monkeypatch.setattr(cli_main._context, "load_config", lambda: config)

    def _raise_config_error(_config: CLIConfig) -> None:
        raise ConfigurationError("API base URL is not configured. Run 'ctxme config --api <url>'.")

    monkeypatch.setattr(cli_main._context.client_factory, "build", _raise_config_error)

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["ls"])

    # Auth check happens before client build, so we get auth error first
    assert result.exit_code == 1
    assert "Not signed in. Run 'ctxme auth login' to authenticate." in str(result.exception)


def test_put_command_reads_from_src(tmp_path, monkeypatch):
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

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

    data_file = tmp_path / "note.txt"
    data_file.write_text("File-based payload")

    runner = CliRunner()
    # Use --src to upload from file (derives key from filename)
    result = runner.invoke(cli_main.app, ["put", "--src", str(data_file)])

    assert result.exit_code == 0
    assert "Saved" in result.stdout


def test_search_command_outputs_results(tmp_path, monkeypatch):
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

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

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["search", "welcome"])

    assert result.exit_code == 0
    assert "Search results" in result.stdout


def test_search_all_flag_searches_across_projects(tmp_path, monkeypatch):
    """Test that --all flag searches across all projects."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    # Search for a term that exists in engineering project
    result = runner.invoke(cli_main.app, ["search", "runbook", "--all"])

    assert result.exit_code == 0
    assert "Search results across all projects" in result.stdout
    assert "engineering" in result.stdout
    assert "runbook" in result.stdout


def test_search_all_flag_returns_results_from_multiple_projects(tmp_path, monkeypatch):
    """Test that --all returns results from multiple projects when query matches both."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    # "context" appears in both:
    # - core/welcome data: "This is a placeholder item used by the CLI and MCP scaffolds."
    #   (title contains "Context Platform")
    # - engineering/runbook data: "Capture context"
    result = runner.invoke(cli_main.app, ["search", "context", "--all"])

    assert result.exit_code == 0
    assert "Search results across all projects" in result.stdout
    # Verify results from BOTH projects appear
    assert "core" in result.stdout
    assert "engineering" in result.stdout


def test_search_all_and_project_flags_mutually_exclusive(tmp_path, monkeypatch):
    """Test that --all and --project cannot be used together."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["search", "test", "--all", "--project", "core"])

    assert result.exit_code == 1
    assert "Cannot use --all with --project" in str(result.exception)


def test_search_all_with_limit_caps_results(tmp_path, monkeypatch):
    """Test that --limit caps the number of results returned."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()

    # First verify "context" matches items in both projects without limit
    result_unlimited = runner.invoke(cli_main.app, ["search", "context", "--all"])
    assert result_unlimited.exit_code == 0
    # Both item keys should appear (welcome from core, runbook from engineering)
    assert "welcome" in result_unlimited.stdout
    assert "runbook" in result_unlimited.stdout

    # Now with limit=1, should only get 1 result
    result_limited = runner.invoke(cli_main.app, ["search", "context", "--all", "--limit", "1"])
    assert result_limited.exit_code == 0
    assert "Search results across all projects" in result_limited.stdout

    # Count item keys - exactly one should appear, not both
    has_welcome = "welcome" in result_limited.stdout
    has_runbook = "runbook" in result_limited.stdout
    assert (
        has_welcome != has_runbook
    ), f"Expected exactly 1 result but got welcome={has_welcome}, runbook={has_runbook}"


def test_search_all_no_matches(tmp_path, monkeypatch):
    """Test --all with no matching results shows appropriate message."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["search", "nonexistentterm12345", "--all"])

    assert result.exit_code == 0
    assert "No matches found across any projects" in result.stdout


def test_search_all_short_flag(tmp_path, monkeypatch):
    """Test that -a short flag works for --all."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["search", "runbook", "-a"])

    assert result.exit_code == 0
    assert "Search results across all projects" in result.stdout


def test_search_explicit_all_no_info_message(tmp_path, monkeypatch):
    """Test that explicit --all flag searches without showing info message."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["search", "welcome", "--all"])

    assert result.exit_code == 0
    assert "Search results across all projects" in result.stdout
    # Should NOT show the informational message for explicit --all
    assert "no default project set" not in result.stdout


def test_search_with_default_project_searches_only_that_project(tmp_path, monkeypatch):
    """Test that search uses default project when configured."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

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

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["search", "welcome"])

    assert result.exit_code == 0
    assert "Search results in core" in result.stdout
    # Should NOT search all projects or show info message
    assert "across all projects" not in result.stdout
    assert "no default project set" not in result.stdout


def test_search_implicit_all_with_info_message(tmp_path, monkeypatch):
    """Test that search defaults to --all with info message when no default project."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))  # No default_project

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["search", "welcome"])

    assert result.exit_code == 0
    # Should show informational message for implicit defaulting
    assert "Searching across all projects (no default project set)" in result.stdout
    assert "Search results across all projects" in result.stdout


def test_search_zero_projects_shows_helpful_message(tmp_path, monkeypatch):
    """Test that search shows helpful message when user has no projects."""
    from unittest.mock import MagicMock, patch

    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    store = ConfigStore()
    store.save(CLIConfig(use_mock_client=True))  # No default_project

    runner = CliRunner()

    # Mock the client's list_projects to return empty list
    with patch("cli.commands.items.authenticated_client") as mock_client_ctx:
        mock_client = MagicMock()
        mock_client.list_projects.return_value = []
        mock_client_ctx.return_value.__enter__.return_value = mock_client

        result = runner.invoke(cli_main.app, ["search", "test"])

    assert result.exit_code == 0
    assert "No projects found" in result.stdout
    assert "ctx projects create" in result.stdout


def test_search_auth_error_surfaces_before_project_config_error(
    tmp_path, monkeypatch, in_memory_keyring
):
    """Test that auth errors surface before project configuration errors."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

    # Configure CLI for live mode without API key
    store = ConfigStore()
    store.save(CLIConfig(api_base_url="http://localhost:8000", use_mock_client=False))

    runner = CliRunner()
    # Try to search without auth and without default project
    result = runner.invoke(cli_main.app, ["search", "test"])

    assert result.exit_code == 1
    # Should get auth error, not project config error
    assert "Not signed in" in str(result.exception) or "authenticate" in str(result.exception)


def test_search_project_flag_overrides_default(tmp_path, monkeypatch):
    """Test that --project flag overrides default project configuration."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

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

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["search", "runbook", "--project", "engineering"])

    assert result.exit_code == 0
    # Should search in engineering, not core (the default)
    assert "Search results in engineering" in result.stdout
    assert "Search results in core" not in result.stdout


def test_search_all_flag_overrides_default(tmp_path, monkeypatch):
    """Test that --all flag overrides default project configuration."""
    monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
    cli_main = _reload_cli_module()

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

    runner = CliRunner()
    result = runner.invoke(cli_main.app, ["search", "welcome", "--all"])

    assert result.exit_code == 0
    # Should search all projects, not just core (the default)
    assert "Search results across all projects" in result.stdout
    assert "Search results in core" not in result.stdout
    # Should NOT show the implicit defaulting message (this is explicit --all)
    assert "no default project set" not in result.stdout
