# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc

from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import urlsplit

import pytest
from typer.testing import CliRunner

import solstone.apps.settings.call as settings_call
import solstone.apps.settings.routes as settings_routes
from solstone.think.convey_client import ConveyClient
from tests._baseline_harness import make_test_client

runner = CliRunner()

API_ENV_KEYS = (
    "GOOGLE_API_KEY",
    "ANTHROPIC_API_KEY",
    "OPENAI_API_KEY",
    "PLAUD_ACCESS_TOKEN",
    "GOOGLE_APPLICATION_CREDENTIALS",
)


class _FixedDateTime:
    @classmethod
    def now(cls, tz=None):
        return datetime(2026, 4, 17, 12, 0, tzinfo=tz or timezone.utc)


class _CaptureSession:
    def __init__(self, inner):
        self.inner = inner
        self.gets: list[str] = []
        self.posts: list[tuple[str, dict[str, Any] | None]] = []

    def get(self, url: str):
        self.gets.append(urlsplit(url).path)
        return self.inner.get(url)

    def post(self, url: str, json=None):
        self.posts.append((urlsplit(url).path, json))
        return self.inner.post(url, json=json)

    def put(self, url: str, json=None):
        return self.inner.put(url, json=json)

    def delete(self, url: str):
        return self.inner.delete(url)


@pytest.fixture(autouse=True)
def _settings_client(journal_copy: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    for key in API_ENV_KEYS:
        monkeypatch.delenv(key, raising=False)
    client = ConveyClient(
        session=make_test_client(journal_copy),
        base_url="",
    )
    monkeypatch.setattr(settings_call, "get_client", lambda: client)
    monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1")


@pytest.fixture
def fake_validators(monkeypatch: pytest.MonkeyPatch) -> None:
    def validate_token(token: str) -> dict[str, Any]:
        return {"valid": True, "token": token[-4:]}

    monkeypatch.setattr(settings_routes, "datetime", _FixedDateTime)
    monkeypatch.setattr("solstone.think.importers.plaud.validate_token", validate_token)


def _read_config(journal: Path) -> dict[str, Any]:
    return json.loads((journal / "config" / "journal.json").read_text(encoding="utf-8"))


def _write_config(journal: Path, payload: dict[str, Any]) -> None:
    (journal / "config" / "journal.json").write_text(
        json.dumps(payload, indent=2) + "\n",
        encoding="utf-8",
    )


def _assert_json(result, expected: Any) -> None:
    assert result.exit_code == 0
    assert json.loads(result.stdout) == expected
    assert result.stderr == ""


def test_show_and_read_verbs_select_http_fields(journal_copy: Path) -> None:
    show = runner.invoke(settings_call.app, ["show"])
    keys = runner.invoke(settings_call.app, ["keys", "show"])
    transcribe = runner.invoke(settings_call.app, ["transcribe", "show"])
    identity = runner.invoke(settings_call.app, ["identity", "show"])
    observer = runner.invoke(settings_call.app, ["observer", "show"])

    assert show.exit_code == 0
    show_payload = json.loads(show.stdout)
    assert list(show_payload) == [
        "identity",
        "transcribe",
        "observe",
        "keys",
    ]
    assert show_payload["identity"]["name"] == "Test User"
    assert list(show_payload["keys"]) == [
        "PLAUD_ACCESS_TOKEN",
    ]

    assert keys.exit_code == 0
    assert json.loads(keys.stdout) == {key: False for key in show_payload["keys"]}
    assert transcribe.exit_code == 0
    assert set(json.loads(transcribe.stdout)) == {"backends", "api_keys", "config"}
    assert identity.exit_code == 0
    assert json.loads(identity.stdout)["name"] == "Test User"
    assert observer.exit_code == 0
    assert json.loads(observer.stdout)["tmux"]["capture_interval"] == 5


def test_settings_config_projects_service_validation_only(journal_copy: Path) -> None:
    config = _read_config(journal_copy)
    config["service_key_validation"] = {
        "plaud": {"valid": False, "error": "bad token"},
    }
    _write_config(journal_copy, config)

    payload = settings_call.get_client().request("GET", "/app/settings/api/config")

    assert "providers" not in payload
    assert payload["key_validation"] == {
        "plaud": {"valid": False, "error": "bad token"},
    }


def test_keys_set_clear_validate_and_invalid_env(
    journal_copy: Path,
    fake_validators: None,
) -> None:
    invalid = runner.invoke(settings_call.app, ["keys", "set", "BOGUS", "value"])
    assert invalid.exit_code == 1
    assert invalid.stderr == (
        "Invalid env var: BOGUS. Must be one of: PLAUD_ACCESS_TOKEN\n"
    )

    ai_key = runner.invoke(
        settings_call.app,
        ["keys", "set", "ANTHROPIC_API_KEY", "anthropic-test-key"],
    )
    assert ai_key.exit_code == 1
    assert ai_key.stderr.startswith("Invalid env var: ANTHROPIC_API_KEY.")
    assert "ANTHROPIC_API_KEY" not in _read_config(journal_copy).get("env", {})

    service_set = runner.invoke(
        settings_call.app,
        ["keys", "set", "PLAUD_ACCESS_TOKEN", "plaud-token"],
    )
    assert service_set.exit_code == 0
    assert json.loads(service_set.stdout) == {
        "env_var": "PLAUD_ACCESS_TOKEN",
        "set": True,
        "validation": {
            "valid": True,
            "token": "oken",
            "timestamp": "2026-04-17T12:00:00+00:00",
        },
    }
    assert _read_config(journal_copy)["service_key_validation"]["plaud"]["valid"]
    keys_shown = runner.invoke(settings_call.app, ["keys", "show"])
    assert keys_shown.exit_code == 0
    assert "plaud-token" not in keys_shown.stdout

    cleared = runner.invoke(settings_call.app, ["keys", "clear", "PLAUD_ACCESS_TOKEN"])
    _assert_json(cleared, {"env_var": "PLAUD_ACCESS_TOKEN", "cleared": True})
    assert _read_config(journal_copy)["env"]["PLAUD_ACCESS_TOKEN"] == ""

    before = (journal_copy / "config" / "journal.json").read_text(encoding="utf-8")
    validate = runner.invoke(settings_call.app, ["keys", "validate"])
    assert validate.exit_code == 0
    assert json.loads(validate.stdout)["key_validation"] == {}
    assert (journal_copy / "config" / "journal.json").read_text(
        encoding="utf-8"
    ) == before

    runner.invoke(
        settings_call.app, ["keys", "set", "PLAUD_ACCESS_TOKEN", "plaud-token"]
    )
    cached = runner.invoke(settings_call.app, ["keys", "validate", "--cache-result"])
    assert cached.exit_code == 0
    assert _read_config(journal_copy)["service_key_validation"]["plaud"]["valid"]


def test_transcribe_setters(journal_copy: Path) -> None:
    transcribe_bad = runner.invoke(
        settings_call.app,
        ["transcribe", "set-backend", "invalid"],
    )
    assert transcribe_bad.exit_code == 1
    assert transcribe_bad.stderr == (
        "Invalid backend: invalid. Must be one of: parakeet, parakeet-cpp\n"
    )

    transcribe_set = runner.invoke(
        settings_call.app,
        ["transcribe", "set-backend", "parakeet-cpp"],
    )
    assert transcribe_set.exit_code == 0
    assert json.loads(transcribe_set.stdout)["backend"] == "parakeet-cpp"
    assert _read_config(journal_copy)["transcribe"]["backend"] == "parakeet-cpp"


def test_processing_show_uses_effective_settings_endpoint(
    journal_copy: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    session = _CaptureSession(make_test_client(journal_copy))
    client = ConveyClient(session=session, base_url="")
    monkeypatch.setattr(settings_call, "get_client", lambda: client)

    result = runner.invoke(settings_call.app, ["processing", "show"])

    assert result.exit_code == 0
    assert session.gets == ["/app/settings/api/processing"]
    payload = json.loads(result.stdout)
    assert payload["mode"] == "realtime"
    assert payload["gate"]["time_window"]["start"] == "02:00"
    assert result.stderr == ""


def test_processing_set_posts_only_supplied_fields(
    journal_copy: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    session = _CaptureSession(make_test_client(journal_copy))
    client = ConveyClient(session=session, base_url="")
    monkeypatch.setattr(settings_call, "get_client", lambda: client)

    result = runner.invoke(
        settings_call.app,
        ["processing", "set", "--mode", "deferred", "--window-start", "03:00"],
    )

    assert result.exit_code == 0
    assert session.posts == [
        (
            "/app/settings/api/config",
            {
                "section": "processing",
                "data": {
                    "mode": "deferred",
                    "gate": {"time_window": {"start": "03:00"}},
                },
            },
        )
    ]
    payload = json.loads(result.stdout)
    assert payload["mode"] == "deferred"
    assert payload["gate"]["time_window"]["start"] == "03:00"
    assert result.stderr == ""


def test_processing_set_surfaces_invalid_config_detail() -> None:
    result = runner.invoke(
        settings_call.app,
        ["processing", "set", "--mode", "bogus"],
    )

    assert result.exit_code == 1
    assert result.stdout == ""
    assert result.stderr == "processing mode must be realtime or deferred\n"


def test_identity_and_observer_setters(journal_copy: Path) -> None:
    bad_pronouns = runner.invoke(
        settings_call.app,
        ["identity", "set", "--pronouns", "{bad"],
    )
    assert bad_pronouns.exit_code == 1
    assert bad_pronouns.stderr == "Invalid JSON in pronouns\n"

    name = runner.invoke(settings_call.app, ["identity", "set", "--name", "New Name"])
    assert name.exit_code == 0
    assert json.loads(name.stdout)["name"] == "New Name"

    add_email = runner.invoke(
        settings_call.app,
        ["identity", "set", "--add-email", "new@example.com"],
    )
    assert add_email.exit_code == 0
    assert "new@example.com" in json.loads(add_email.stdout)["email_addresses"]

    remove_email = runner.invoke(
        settings_call.app,
        ["identity", "set", "--remove-email", "test@example.com"],
    )
    assert remove_email.exit_code == 0
    assert "test@example.com" not in json.loads(remove_email.stdout)["email_addresses"]

    observer_bad = runner.invoke(
        settings_call.app,
        ["observer", "set", "--capture-interval", "100"],
    )
    assert observer_bad.exit_code == 1
    assert observer_bad.stderr == (
        "tmux.capture_interval must be an integer between 1 and 60\n"
    )

    observer_set = runner.invoke(
        settings_call.app,
        ["observer", "set", "--no-enabled", "--capture-interval", "10"],
    )
    assert observer_set.exit_code == 0
    assert json.loads(observer_set.stdout) == {
        "capture_interval": 10,
        "enabled": False,
    }
    assert _read_config(journal_copy)["observe"]["tmux"] == {
        "capture_interval": 10,
        "enabled": False,
    }


def test_convey_status_dashboard_url(
    journal_copy: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    status = runner.invoke(settings_call.app, ["convey", "status"])
    assert status.exit_code == 0
    assert status.stdout == (
        "convey\n"
        "  bind:              127.0.0.1:5015\n"
        "  dashboard url:     http://localhost:5015\n"
    )

    config_before = _read_config(journal_copy)
    manual_status = runner.invoke(settings_call.app, ["convey", "status"])
    assert manual_status.exit_code == 0
    assert "dashboard url:     http://localhost:5015" in manual_status.stdout
    assert _read_config(journal_copy) == config_before

    calls = []
    with monkeypatch.context() as m:
        m.setattr(settings_call, "get_client", lambda: calls.append("called"))
        missing = runner.invoke(settings_call.app, ["convey", "host-url"])
    assert missing.exit_code != 0
    assert "No such command" in missing.stderr
    assert calls == []

    client = make_test_client(journal_copy)
    response = client.get("/app/settings/api/convey/host-url")
    assert response.status_code == 404
