"""Tests for spens.init (config generation, gitignore, CLI behavior)."""

import json
from pathlib import Path

import pytest
from spens.config import CONFIG_FILENAME
from spens.init import (
    InitAnswers,
    InitError,
    generate_config,
    parse_providers,
    run_init,
    update_gitignore,
)


def _find_rule(config: dict, pattern: str) -> dict | None:
    for rule in config["domain_rules"]:
        if rule["pattern"] == pattern:
            return rule
    return None


# ---------------------------------------------------------------------------
# generate_config
# ---------------------------------------------------------------------------


def test_stack_rules_added_when_tooling_allowed() -> None:
    config = generate_config(InitAnswers(stack="node", providers=["manual"]))
    rule = _find_rule(config, "*registry.npmjs.org")
    assert rule is not None
    assert rule["allow"] == ["GET", "HEAD", "POST", "PUT"]
    assert _find_rule(config, "*pypi.org") is None  # other stacks not merged


def test_stack_rules_omitted_when_tooling_denied() -> None:
    answers = InitAnswers(stack="node", providers=["manual"], tooling_urls=False)
    config = generate_config(answers)
    assert config["domain_rules"] == []


def test_manual_stack_configures_nothing() -> None:
    answers = InitAnswers(stack="manual", providers=["manual"])
    config = generate_config(answers)
    assert config["domain_rules"] == []
    assert config["addition_capture_urls"] == []
    assert config["exclude_capture_urls"] == []
    assert "env" not in config
    assert "inject_headers" not in config


def test_public_get_adds_wildcard_get_rule() -> None:
    answers = InitAnswers(stack="manual", providers=["manual"], public_get=True)
    config = generate_config(answers)
    assert _find_rule(config, "*") == {"pattern": "*", "allow": ["GET"]}


def test_provider_rules_and_capture_urls() -> None:
    answers = InitAnswers(stack="manual", providers=["anthropic"])
    config = generate_config(answers)
    rule = _find_rule(config, "*api.anthropic.com")
    assert rule is not None
    assert "POST" in rule["allow"]
    assert "*api.anthropic.com*" in config["addition_capture_urls"]


def test_provider_env_injected_when_allowed() -> None:
    answers = InitAnswers(stack="manual", providers=["anthropic"])
    config = generate_config(answers)
    assert config["env"] == ["ANTHROPIC_BASE_URL", "ANTHROPIC_MODEL", "ANTHROPIC_VERSION"]
    placeholders = {r["placeholder"] for r in config["inject_headers"]}
    assert placeholders == {"ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"}
    for rule in config["inject_headers"]:
        assert rule["for_domains"] == ["*api.anthropic.com*"]
        assert rule["env_var"] == rule["placeholder"]


def test_provider_env_omitted_when_denied() -> None:
    answers = InitAnswers(stack="manual", providers=["openai"], inject_env=False)
    config = generate_config(answers)
    assert "env" not in config
    assert "inject_headers" not in config
    # domain rules / capture urls are still configured
    assert _find_rule(config, "*api.openai.com") is not None


def test_multiple_providers_are_merged() -> None:
    answers = InitAnswers(stack="manual", providers=["openai", "fireworks"])
    config = generate_config(answers)
    assert _find_rule(config, "*api.openai.com") is not None
    assert _find_rule(config, "*api.fireworks.ai") is not None
    assert config["addition_capture_urls"] == ["*api.openai.com*", "*api.fireworks.ai*"]
    assert config["env"] == [
        "OPENAI_BASE_URL", "OPENAI_ORGANIZATION", "OPENAI_PROJECT",
        "FIREWORKS_BASE_URL", "FIREWORKS_ACCOUNT_ID",
    ]
    assert len(config["inject_headers"]) == 2


def test_duplicate_providers_are_not_duplicated() -> None:
    answers = InitAnswers(stack="manual", providers=["openai", "openai"])
    config = generate_config(answers)
    assert config["addition_capture_urls"] == ["*api.openai.com*"]
    assert len(config["inject_headers"]) == 1


def test_combined_stack_and_provider_config() -> None:
    answers = InitAnswers(stack="python", providers=["anthropic"], public_get=True)
    config = generate_config(answers)
    patterns = [r["pattern"] for r in config["domain_rules"]]
    # public wildcard rule first, then stack, then provider
    assert patterns[0] == "*"
    assert "*pypi.org" in patterns
    assert "*api.anthropic.com" in patterns


def test_unknown_stack_raises() -> None:
    with pytest.raises(InitError):
        generate_config(InitAnswers(stack="cobol", providers=["manual"]))


def test_unknown_provider_raises() -> None:
    with pytest.raises(InitError):
        generate_config(InitAnswers(stack="manual", providers=["unknown"]))


# ---------------------------------------------------------------------------
# parse_providers
# ---------------------------------------------------------------------------


def test_parse_providers_single_and_multiple() -> None:
    assert parse_providers("openai") == ["openai"]
    assert parse_providers("openai, fireworks") == ["openai", "fireworks"]
    assert parse_providers("openai,openai") == ["openai"]


def test_parse_providers_rejects_unknown() -> None:
    with pytest.raises(InitError):
        parse_providers("openai,unknown")


def test_parse_providers_rejects_manual_combined() -> None:
    with pytest.raises(InitError):
        parse_providers("manual,openai")


def test_parse_providers_rejects_empty() -> None:
    with pytest.raises(InitError):
        parse_providers(" , ")


# ---------------------------------------------------------------------------
# update_gitignore
# ---------------------------------------------------------------------------


def test_gitignore_created_when_missing(tmp_path: Path) -> None:
    assert update_gitignore(tmp_path) is True
    assert (tmp_path / ".gitignore").read_text(encoding="utf-8") == ".spens\n"


def test_gitignore_entry_appended_when_missing(tmp_path: Path) -> None:
    gitignore = tmp_path / ".gitignore"
    gitignore.write_text("node_modules\n", encoding="utf-8")
    assert update_gitignore(tmp_path) is True
    assert gitignore.read_text(encoding="utf-8") == "node_modules\n.spens\n"


def test_gitignore_appends_newline_when_needed(tmp_path: Path) -> None:
    gitignore = tmp_path / ".gitignore"
    gitignore.write_text("node_modules", encoding="utf-8")  # no trailing newline
    assert update_gitignore(tmp_path) is True
    assert gitignore.read_text(encoding="utf-8") == "node_modules\n.spens\n"


def test_gitignore_noop_when_entry_present(tmp_path: Path) -> None:
    gitignore = tmp_path / ".gitignore"
    gitignore.write_text("node_modules\n.spens\n", encoding="utf-8")
    assert update_gitignore(tmp_path) is False
    assert gitignore.read_text(encoding="utf-8") == "node_modules\n.spens\n"


# ---------------------------------------------------------------------------
# run_init (CLI)
# ---------------------------------------------------------------------------


class _ScriptedPrompter:
    """Prompter returning scripted answers, recording every question."""

    def __init__(self, answers: dict[str, object]) -> None:
        self._answers = answers
        self.questions: list[str] = []

    def choice(self, question, options):  # noqa: ANN001
        self.questions.append(question)
        return self._answers["stack"]

    def multiple_choice(self, question, options):  # noqa: ANN001
        self.questions.append(question)
        return self._answers["providers"]

    def boolean(self, question, default=False):  # noqa: ANN001
        self.questions.append(question)
        return self._answers.get(question, default)


def test_yes_writes_config_with_defaults(tmp_path: Path) -> None:
    code = run_init(
        ["--yes", "--stack", "node", "--provider", "openai"], cwd=tmp_path
    )
    assert code == 0
    config = json.loads(
        (tmp_path / CONFIG_FILENAME).read_text(encoding="utf-8")
    )
    # Q2 default no -> no wildcard rule; Q3 default yes -> stack rules present
    assert _find_rule(config, "*") is None
    assert _find_rule(config, "*registry.npmjs.org") is not None
    # Q5 default yes -> provider env configured
    assert config["env"] == ["OPENAI_BASE_URL", "OPENAI_ORGANIZATION", "OPENAI_PROJECT"]
    assert (tmp_path / ".gitignore").read_text(encoding="utf-8") == ".spens\n"


def test_yes_requires_stack_and_provider(tmp_path: Path) -> None:
    assert run_init(["--yes"], cwd=tmp_path) == 1
    assert run_init(["--yes", "--stack", "node"], cwd=tmp_path) == 1
    assert run_init(["--yes", "--provider", "openai"], cwd=tmp_path) == 1
    assert not (tmp_path / CONFIG_FILENAME).exists()


def test_dry_run_prints_config_without_writing(
    tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
    code = run_init(
        ["--dry-run", "--yes", "--stack", "manual", "--provider", "manual"],
        cwd=tmp_path,
    )
    assert code == 0
    printed = json.loads(capsys.readouterr().out)
    assert printed["domain_rules"] == []
    assert not (tmp_path / CONFIG_FILENAME).exists()
    assert not (tmp_path / ".gitignore").exists()


def test_flag_overrides_beat_defaults(tmp_path: Path) -> None:
    code = run_init(
        [
            "--yes", "--stack", "node", "--provider", "openai",
            "--public-get", "--no-tooling-urls", "--no-inject-env",
        ],
        cwd=tmp_path,
    )
    assert code == 0
    config = json.loads((tmp_path / CONFIG_FILENAME).read_text(encoding="utf-8"))
    assert _find_rule(config, "*") is not None
    assert _find_rule(config, "*registry.npmjs.org") is None
    assert "env" not in config
    assert "inject_headers" not in config


def test_custom_output_path(tmp_path: Path) -> None:
    code = run_init(
        ["--yes", "--stack", "manual", "--provider", "manual",
         "--output", str(tmp_path / "configs" / "spens.config.json")],
        cwd=tmp_path,
    )
    assert code == 0
    assert (tmp_path / "configs" / "spens.config.json").is_file()


def test_existing_config_requires_confirmation(tmp_path: Path, monkeypatch) -> None:
    config_path = tmp_path / CONFIG_FILENAME
    config_path.write_text('{"domain_rules": []}', encoding="utf-8")
    monkeypatch.setattr("builtins.input", lambda *a: "n")
    assert run_init(
        ["--yes", "--stack", "node", "--provider", "openai"], cwd=tmp_path
    ) == 1  # --yes never prompts for overwrite
    assert config_path.read_text(encoding="utf-8") == '{"domain_rules": []}'


def test_existing_config_confirmed_overwrites(tmp_path: Path, monkeypatch) -> None:
    config_path = tmp_path / CONFIG_FILENAME
    config_path.write_text('{"domain_rules": []}', encoding="utf-8")
    monkeypatch.setattr("builtins.input", lambda *a: "y")
    assert run_init(["--stack", "node", "--provider", "openai"], cwd=tmp_path) == 0
    config = json.loads(config_path.read_text(encoding="utf-8"))
    assert _find_rule(config, "*registry.npmjs.org") is not None


def test_force_overwrites_existing_config(tmp_path: Path) -> None:
    config_path = tmp_path / CONFIG_FILENAME
    config_path.write_text('{"domain_rules": []}', encoding="utf-8")
    code = run_init(
        ["--yes", "--force", "--stack", "node", "--provider", "manual"],
        cwd=tmp_path,
    )
    assert code == 0
    config = json.loads(config_path.read_text(encoding="utf-8"))
    assert _find_rule(config, "*registry.npmjs.org") is not None


def test_interactive_flow_uses_prompts(tmp_path: Path) -> None:
    prompter = _ScriptedPrompter({
        "stack": "rust",
        "providers": ["openai", "fireworks"],
    })
    code = run_init([], cwd=tmp_path, prompter=prompter)
    assert code == 0
    # All five questions asked (nothing answered via flags).
    assert len(prompter.questions) == 5
    config = json.loads((tmp_path / CONFIG_FILENAME).read_text(encoding="utf-8"))
    assert _find_rule(config, "*crates.io") is not None
    assert _find_rule(config, "*api.openai.com") is not None
    assert _find_rule(config, "*api.fireworks.ai") is not None


def test_interactive_manual_stack_skips_tooling_question(tmp_path: Path) -> None:
    prompter = _ScriptedPrompter({
        "stack": "manual",
        "providers": ["manual"],
    })
    code = run_init([], cwd=tmp_path, prompter=prompter)
    assert code == 0
    # Stack + provider + public-get asked; tooling (stack=manual) and env
    # (manual provider) omitted from the flow.
    assert len(prompter.questions) == 3


def test_flags_answer_some_questions_only(tmp_path: Path) -> None:
    prompter = _ScriptedPrompter({
        "providers": ["anthropic"],
    })
    code = run_init(
        ["--stack", "python", "--no-public-get", "--no-tooling-urls",
         "--no-inject-env"],
        cwd=tmp_path,
        prompter=prompter,
    )
    assert code == 0
    # Only the provider question remained unanswered.
    assert prompter.questions.count("What model provider are you using?") == 1
    assert len(prompter.questions) == 1
    config = json.loads((tmp_path / CONFIG_FILENAME).read_text(encoding="utf-8"))
    # tooling URLs denied via flag -> no stack rules, provider rules remain
    assert _find_rule(config, "*pypi.org") is None
    assert _find_rule(config, "*api.anthropic.com") is not None
    assert "env" not in config
