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

from __future__ import annotations

import hashlib
import json
import logging
import stat
import threading

import pytest

from solstone.think.journal_config import JournalConfigMutation, mutate_journal_config
from solstone.think.services import scout as scout_module
from solstone.think.services import status as service_status
from solstone.think.services.scout import (
    KEY_FINGERPRINT_FIELD,
    DisableOutcome,
    JournalNotInitializedError,
    approved_dispatch_token,
    disable_scout,
    get_scout_dispatch_token,
    is_manual_key_present,
    is_scout_enabled,
    provision_scout_handoff,
    record_scout_pending,
    scout_provenance,
)
from tests.helpers.journal_config import seed_journal_config


def _payload(suffix: str = "one") -> dict[str, str]:
    return {
        "google_api_key": f"google-{suffix}",
        "dispatch_token": f"dispatch-{suffix}",
        "account_id": f"acct-{suffix}",
        "created_at": f"2026-05-24T00:00:0{len(suffix)}Z",
    }


def _config_path(journal_copy):
    return journal_copy / "config" / "journal.json"


def _read_config(journal_copy):
    return json.loads(_config_path(journal_copy).read_text())


def test_provision_scout_handoff_round_trip_preserves_config(journal_copy) -> None:
    before = _read_config(journal_copy)

    provision_scout_handoff(_payload())

    after = _read_config(journal_copy)
    assert after["identity"] == before["identity"]
    if "retention" in before:
        assert after["retention"] == before["retention"]
    assert after["convey"] == before["convey"]
    assert after["env"]["GOOGLE_API_KEY"] == "google-one"
    scout = after["services"]["scout"]
    assert set(scout) == {
        "enabled_at",
        "account_id",
        "key_created_at",
        "dispatch_token",
        KEY_FINGERPRINT_FIELD,
    }
    assert scout["account_id"] == "acct-one"
    assert scout["key_created_at"] == _payload()["created_at"]
    assert scout["dispatch_token"] == "dispatch-one"
    assert scout[KEY_FINGERPRINT_FIELD] == hashlib.sha256(b"google-one").hexdigest()
    assert scout_provenance() == after["services"]["scout"]
    assert stat.S_IMODE(_config_path(journal_copy).stat().st_mode) == 0o600


def test_redact_handoff_redacts_secrets_keeps_nonsecrets() -> None:
    payload = {
        "google_api_key": "google-secret",
        "dispatch_token": "dispatch-secret",
        "account_id": "acct-visible",
        "created_at": 1716508800,
    }

    redacted = scout_module._redact_handoff(payload)

    assert redacted["google_api_key"] == "***redacted***"
    assert redacted["dispatch_token"] == "***redacted***"
    assert redacted["account_id"] == "acct-visible"
    assert redacted["created_at"] == 1716508800
    assert isinstance(redacted["created_at"], int)
    assert "google-secret" not in redacted.values()
    assert "dispatch-secret" not in redacted.values()


def test_provision_scout_handoff_debug_log_redacts_payload(
    journal_copy, caplog
) -> None:
    caplog.set_level(logging.DEBUG)

    provision_scout_handoff(_payload())

    assert "received scout handoff payload" in caplog.text
    assert "***redacted***" in caplog.text
    assert "google-one" not in caplog.text
    assert "dispatch-one" not in caplog.text


def test_scout_three_state_matrix(journal_copy) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    config.pop("services", None)
    seed_journal_config(config, journal_copy)
    assert not is_scout_enabled()
    assert not is_manual_key_present()
    assert scout_provenance() is None

    config = _read_config(journal_copy)
    config.setdefault("env", {})["GOOGLE_API_KEY"] = "manual"
    seed_journal_config(config, journal_copy)
    assert not is_scout_enabled()
    assert is_manual_key_present()

    provision_scout_handoff(_payload("two"))
    assert is_scout_enabled()
    assert not is_manual_key_present()


def test_pending_marker_alone_predicates(journal_copy) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    config.pop("services", None)
    seed_journal_config(config, journal_copy)

    record_scout_pending("acct-p", 1_700_000_000_000)

    assert not is_scout_enabled()
    assert not is_manual_key_present()
    saved = _read_config(journal_copy)
    assert "GOOGLE_API_KEY" not in saved.get("env", {})
    scout = dict(saved["services"]["scout"])
    checked_at = scout.pop("checked_at")
    assert isinstance(checked_at, str)
    assert scout == {
        "state": "pending",
        "account_id": "acct-p",
        "since": 1_700_000_000_000,
    }
    assert scout_provenance() == saved["services"]["scout"]


def test_pending_marker_with_manual_key_predicates(journal_copy) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {})["GOOGLE_API_KEY"] = "manual"
    config.pop("services", None)
    seed_journal_config(config, journal_copy)

    record_scout_pending("acct-p", 1_700_000_000_000)

    assert not is_scout_enabled()
    assert is_manual_key_present()
    saved = _read_config(journal_copy)
    assert saved["env"]["GOOGLE_API_KEY"] == "manual"


def test_record_scout_pending_requires_account_id(journal_copy) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    config.pop("services", None)
    seed_journal_config(config, journal_copy)

    with pytest.raises(ValueError):
        record_scout_pending("", 123)
    with pytest.raises(ValueError):
        record_scout_pending(None, 123)

    assert "scout" not in _read_config(journal_copy).get("services", {})


def test_record_scout_pending_writes_no_google_api_key(journal_copy) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    config.pop("services", None)
    seed_journal_config(config, journal_copy)

    record_scout_pending("acct-p", 1_700_000_000_000)

    saved = _read_config(journal_copy)
    assert "GOOGLE_API_KEY" not in saved.get("env", {})
    scout = saved["services"]["scout"]
    assert "google_api_key" not in scout
    assert "dispatch_token" not in scout


def test_record_scout_pending_stores_non_empty_dispatch_token(journal_copy) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    config.pop("services", None)
    seed_journal_config(config, journal_copy)

    record_scout_pending("acct-p", 1_700_000_000_000, "dispatch-p")

    saved = _read_config(journal_copy)
    scout = saved["services"]["scout"]
    assert scout["dispatch_token"] == "dispatch-p"
    assert "google_api_key" not in scout
    view = service_status.scout_provenance_view()
    serialized = json.dumps(view).lower()
    assert "dispatch_token" not in view
    assert "dispatch-p" not in serialized
    assert "acct-p" not in serialized


@pytest.mark.parametrize("dispatch_token", [None, "", 123])
def test_record_scout_pending_ignores_invalid_dispatch_token(
    journal_copy,
    dispatch_token,
) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    config.pop("services", None)
    seed_journal_config(config, journal_copy)

    record_scout_pending("acct-p", 1_700_000_000_000, dispatch_token)

    assert "dispatch_token" not in _read_config(journal_copy)["services"]["scout"]


def test_get_scout_dispatch_token_reads_pending_and_approved_blocks(
    journal_copy,
) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    config.pop("services", None)
    seed_journal_config(config, journal_copy)
    assert get_scout_dispatch_token() is None

    record_scout_pending("acct-p", 1_700_000_000_000)
    assert get_scout_dispatch_token() is None

    record_scout_pending("acct-p", 1_700_000_000_000, "dispatch-p")
    assert get_scout_dispatch_token() == "dispatch-p"

    provision_scout_handoff(_payload("approved"))
    assert get_scout_dispatch_token() == "dispatch-approved"


def test_approved_dispatch_token_is_approved_only_and_keeps_stale_push_behavior(
    journal_copy,
) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    config.pop("services", None)
    seed_journal_config(config, journal_copy)
    assert approved_dispatch_token() is None

    record_scout_pending("acct-p", 1_700_000_000_000, "dispatch-p")
    assert approved_dispatch_token() is None

    provision_scout_handoff(_payload("approved"))
    assert approved_dispatch_token() == "dispatch-approved"

    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    seed_journal_config(config, journal_copy)
    assert approved_dispatch_token() == "dispatch-approved"


@pytest.mark.parametrize("field", list(_payload().keys()))
def test_payload_validation_missing_field(journal_copy, field: str) -> None:
    payload = _payload()
    payload.pop(field)

    with pytest.raises(
        ValueError, match=f"malformed handoff payload: missing field '{field}'"
    ):
        provision_scout_handoff(payload)


@pytest.mark.parametrize("field", list(_payload().keys()))
@pytest.mark.parametrize("value", [None, 123, ""])
def test_payload_validation_non_empty_string(journal_copy, field: str, value) -> None:
    payload = _payload()
    payload[field] = value

    with pytest.raises(
        ValueError,
        match=f"malformed handoff payload: field '{field}' must be a non-empty string",
    ):
        provision_scout_handoff(payload)


@pytest.mark.parametrize(
    ("field", "bad_value"),
    [
        ("google_api_key", []),
        ("google_api_key", {}),
        ("google_api_key", 123),
        ("dispatch_token", None),
        ("account_id", ["x"]),
        ("created_at", {"nested": True}),
    ],
)
def test_payload_validation_wrong_type_values(
    journal_copy, field: str, bad_value
) -> None:
    payload = _payload()
    payload[field] = bad_value

    with pytest.raises(ValueError):
        provision_scout_handoff(payload)


def test_provision_preserves_other_env_and_top_level_keys(journal_copy) -> None:
    config = _read_config(journal_copy)
    config["env"] = {
        "GOOGLE_API_KEY": "old",
        "ANTHROPIC_API_KEY": "keep-me",
        "OPENAI_API_KEY": "keep-me-too",
    }
    config["convey"] = {"port": 5015}
    config["custom_block"] = {"survives": True}
    seed_journal_config(config, journal_copy)

    provision_scout_handoff(_payload("new"))

    saved = _read_config(journal_copy)
    assert saved["env"]["GOOGLE_API_KEY"] == "google-new"
    assert saved["env"]["ANTHROPIC_API_KEY"] == "keep-me"
    assert saved["env"]["OPENAI_API_KEY"] == "keep-me-too"
    assert saved["convey"] == {"port": 5015}
    assert saved["custom_block"] == {"survives": True}


def test_disable_scout_when_enabled_returns_outcome_clears_block_and_env_key(
    journal_copy,
) -> None:
    provision_scout_handoff(_payload("disable"))

    outcome = disable_scout()

    assert outcome == DisableOutcome(was_enabled=True, env_key_preserved=False)
    saved = _read_config(journal_copy)
    assert "GOOGLE_API_KEY" not in saved["env"]
    assert saved["services"] == {}


def test_disable_scout_preserves_env_key_when_fingerprint_mismatches(
    journal_copy,
) -> None:
    provision_scout_handoff(_payload("manual"))
    config = _read_config(journal_copy)
    config["env"]["GOOGLE_API_KEY"] = "manual-replacement"
    seed_journal_config(config, journal_copy)

    outcome = disable_scout()

    assert outcome == DisableOutcome(was_enabled=True, env_key_preserved=True)
    saved = _read_config(journal_copy)
    assert saved["env"]["GOOGLE_API_KEY"] == "manual-replacement"
    assert saved["services"] == {}


def test_disable_scout_when_not_enabled_returns_was_enabled_false(
    journal_copy,
) -> None:
    config = _read_config(journal_copy)
    config.setdefault("env", {}).pop("GOOGLE_API_KEY", None)
    config.setdefault("services", {}).pop("scout", None)
    seed_journal_config(config, journal_copy)

    outcome = disable_scout()

    assert outcome == DisableOutcome(was_enabled=False, env_key_preserved=False)
    saved = _read_config(journal_copy)
    assert saved["services"] == {}


def test_provision_requires_initialized_journal(tmp_path, monkeypatch) -> None:
    journal = tmp_path / "journal"
    (journal / "config").mkdir(parents=True)
    monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal))

    with pytest.raises(JournalNotInitializedError):
        provision_scout_handoff(_payload())


def test_locked_parallel_writes_do_not_corrupt_config(journal_copy) -> None:
    errors: list[BaseException] = []

    def write_payload(suffix: str) -> None:
        try:
            provision_scout_handoff(_payload(suffix))
        except BaseException as exc:  # pragma: no cover - asserted below
            errors.append(exc)

    threads = [
        threading.Thread(target=write_payload, args=("alpha",)),
        threading.Thread(target=write_payload, args=("bravo",)),
    ]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()

    assert errors == []
    config = _read_config(journal_copy)
    scout = config["services"]["scout"]
    assert scout["account_id"] in {"acct-alpha", "acct-bravo"}
    suffix = scout["account_id"].removeprefix("acct-")
    assert config["env"]["GOOGLE_API_KEY"] == f"google-{suffix}"


def test_atomic_write_leaves_existing_config_on_replace_failure(
    journal_copy, monkeypatch
) -> None:
    original_text = _config_path(journal_copy).read_text()

    def fail_replace(_tmp, _path) -> None:
        raise OSError("replace failed")

    monkeypatch.setattr("solstone.think.journal_io.atomic.os.replace", fail_replace)

    def apply(draft: dict) -> JournalConfigMutation[None]:
        draft.setdefault("env", {})["GOOGLE_API_KEY"] = "after"
        return JournalConfigMutation(changed=True, value=None)

    with pytest.raises(OSError, match="replace failed"):
        mutate_journal_config(apply)

    assert _config_path(journal_copy).read_text() == original_text
    assert list(_config_path(journal_copy).parent.glob(".tmp_*")) == []
