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

"""AuthorizedClients add/remove/reload semantics for the solstone fork."""

from __future__ import annotations

import datetime as dt
import json
import time
from pathlib import Path

import pytest

from solstone.think.link.auth import MAX_DEVICE_LABEL_LEN, AuthorizedClients


def test_empty_file_is_empty(tmp_path: Path) -> None:
    store = AuthorizedClients(tmp_path / "auth.json")

    assert not store.is_authorized("sha256:abc")


def test_add_and_authorized(tmp_path: Path) -> None:
    store = AuthorizedClients(tmp_path / "auth.json")

    store.add("sha256:abc", "Jer's phone", "inst-1")

    assert store.is_authorized("sha256:abc")
    assert not store.is_authorized("sha256:xyz")


def test_remove(tmp_path: Path) -> None:
    store = AuthorizedClients(tmp_path / "auth.json")
    store.add("sha256:abc", "Jer", "inst-1")

    assert store.remove("sha256:abc") is True
    assert not store.is_authorized("sha256:abc")
    assert store.remove("sha256:abc") is False


def test_external_edit_reloads_on_mtime_change(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)
    store.add("sha256:abc", "Jer", "inst-1")
    assert store.is_authorized("sha256:abc")

    time.sleep(0.02)
    path.write_text(json.dumps([], indent=2) + "\n", encoding="utf-8")

    assert store.reload_if_stale() is True
    assert not store.is_authorized("sha256:abc")


def test_is_authorized_reloads_automatically(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)

    time.sleep(0.02)
    path.write_text(
        json.dumps(
            [
                {
                    "fingerprint": "sha256:zzz",
                    "device_label": "external",
                    "paired_at": "2026-04-19T00:00:00Z",
                    "instance_id": "inst-1",
                }
            ],
            indent=2,
        )
        + "\n",
        encoding="utf-8",
    )

    assert store.is_authorized("sha256:zzz")


def test_snapshot_returns_entries(tmp_path: Path) -> None:
    store = AuthorizedClients(tmp_path / "auth.json")
    store.add("sha256:a", "d1", "inst-1")
    store.add("sha256:b", "d2", "inst-1")

    snapshot = store.snapshot()
    fingerprints = sorted(entry.fingerprint for entry in snapshot)

    assert fingerprints == ["sha256:a", "sha256:b"]
    assert {entry.role for entry in snapshot} == {""}


def test_add_then_last_seen_key_absent_in_payload(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)

    store.add("sha256:abc", "Jer", "inst-1")

    payload = _load_payload(path)
    assert payload[0]["role"] == ""
    assert "last_seen_at" not in payload[0]
    assert "client_label" not in payload[0]


def test_network_round_trips(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)

    store.add("sha256:abc", "Jer", "inst-1", network="anywhere")

    payload = _load_payload(path)
    assert payload[0]["network"] == "anywhere"

    reloaded = AuthorizedClients(path).get("sha256:abc")
    assert reloaded is not None
    assert reloaded.network == "anywhere"


def test_client_label_round_trips(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)

    store.add("sha256:abc", "Assigned", "inst-1", client_label="client-host")

    payload = _load_payload(path)
    assert payload[0]["client_label"] == "client-host"

    reloaded = AuthorizedClients(path).get("sha256:abc")
    assert reloaded is not None
    assert reloaded.client_label == "client-host"


def test_missing_network_defaults_to_none(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    path.write_text(
        json.dumps(
            [
                {
                    "fingerprint": "sha256:abc",
                    "device_label": "Jer",
                    "paired_at": "2026-04-19T00:00:00Z",
                    "instance_id": "inst-1",
                }
            ],
            indent=2,
        )
        + "\n",
        encoding="utf-8",
    )

    entry = AuthorizedClients(path).get("sha256:abc")

    assert entry is not None
    assert entry.network is None


def test_missing_client_label_defaults_to_empty(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    path.write_text(
        json.dumps(
            [
                {
                    "fingerprint": "sha256:abc",
                    "device_label": "Jer",
                    "paired_at": "2026-04-19T00:00:00Z",
                    "instance_id": "inst-1",
                }
            ],
            indent=2,
        )
        + "\n",
        encoding="utf-8",
    )

    entry = AuthorizedClients(path).get("sha256:abc")

    assert entry is not None
    assert entry.client_label == ""


def test_touch_last_seen_unknown_fp_returns_false(tmp_path: Path) -> None:
    store = AuthorizedClients(tmp_path / "auth.json")

    assert store.touch_last_seen("sha256:deadbeef") is False

    store.add("sha256:abc", "Jer", "inst-1")

    assert store.touch_last_seen("sha256:deadbeef") is False


def test_touch_last_seen_updates_timestamp(tmp_path: Path) -> None:
    store = AuthorizedClients(tmp_path / "auth.json")
    fingerprint = "sha256:abc"
    later = dt.datetime(2026, 4, 19, 18, 3, 12, tzinfo=dt.UTC)

    store.add(fingerprint, "Jer", "inst-1")

    assert store.touch_last_seen(fingerprint) is True
    first_entry = next(
        entry for entry in store.snapshot() if entry.fingerprint == fingerprint
    )
    assert first_entry.last_seen_at is not None

    assert store.touch_last_seen(fingerprint, now=later) is True
    second_entry = next(
        entry for entry in store.snapshot() if entry.fingerprint == fingerprint
    )
    assert second_entry.last_seen_at == "2026-04-19T18:03:12Z"


def test_touch_last_seen_persists_key_in_payload(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)

    store.add("sha256:abc", "Jer", "inst-1")
    assert store.touch_last_seen("sha256:abc") is True

    payload = _load_payload(path)
    assert payload[0]["role"] == ""
    assert payload[0]["last_seen_at"]


def test_touch_last_seen_preserves_network(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)

    store.add("sha256:abc", "Jer", "inst-1", network="anywhere")
    assert store.touch_last_seen("sha256:abc") is True

    entry = store.get("sha256:abc")
    assert entry is not None
    assert entry.network == "anywhere"
    assert _load_payload(path)[0]["network"] == "anywhere"


def test_update_label_updates_and_persists(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)
    fingerprint = "sha256:abc"

    store.add(fingerprint, "old name", "inst-1")

    assert store.update_label(fingerprint, "  new name  ") is True
    entry = store.get(fingerprint)
    assert entry is not None
    assert entry.device_label == "new name"

    payload = _load_payload(path)
    assert payload[0]["device_label"] == "new name"


def test_update_label_preserves_network(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)

    store.add("sha256:abc", "old name", "inst-1", network="anywhere")
    assert store.update_label("sha256:abc", "new name") is True

    entry = store.get("sha256:abc")
    assert entry is not None
    assert entry.device_label == "new name"
    assert entry.network == "anywhere"
    assert _load_payload(path)[0]["network"] == "anywhere"


def test_update_label_preserves_client_label(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)

    store.add("sha256:abc", "old name", "inst-1", client_label="client-host")
    assert store.update_label("sha256:abc", "new name") is True

    entry = store.get("sha256:abc")
    assert entry is not None
    assert entry.device_label == "new name"
    assert entry.client_label == "client-host"
    assert _load_payload(path)[0]["client_label"] == "client-host"


@pytest.mark.parametrize(
    ("label", "message"),
    [
        ("", "label must not be empty"),
        ("   ", "label must not be empty"),
        ("x" * (MAX_DEVICE_LABEL_LEN + 1), "label too long"),
    ],
)
def test_update_label_rejects_invalid_labels(
    tmp_path: Path,
    label: str,
    message: str,
) -> None:
    store = AuthorizedClients(tmp_path / "auth.json")
    store.add("sha256:abc", "old name", "inst-1")

    with pytest.raises(ValueError, match=message):
        store.update_label("sha256:abc", label)


def test_update_label_unknown_fp_returns_false(tmp_path: Path) -> None:
    store = AuthorizedClients(tmp_path / "auth.json")

    assert store.update_label("sha256:deadbeef", "new name") is False

    store.add("sha256:abc", "old name", "inst-1")

    assert store.update_label("sha256:deadbeef", "new name") is False


def test_update_label_rereads_file_and_preserves_interleaved_last_seen(
    tmp_path: Path,
) -> None:
    path = tmp_path / "auth.json"
    first = AuthorizedClients(path)
    second = AuthorizedClients(path)
    fingerprint = "sha256:abc"
    seen_at = dt.datetime(2026, 4, 19, 18, 3, 12, tzinfo=dt.UTC)

    first.add(fingerprint, "old name", "inst-1")
    assert second.touch_last_seen(fingerprint, now=seen_at) is True
    assert first.update_label(fingerprint, "new name") is True

    final = AuthorizedClients(path).get(fingerprint)
    assert final is not None
    assert final.device_label == "new name"
    assert final.last_seen_at == "2026-04-19T18:03:12Z"


def test_find_by_label(tmp_path: Path) -> None:
    path = tmp_path / "auth.json"
    store = AuthorizedClients(path)

    assert store.find_by_label("Jer") is None

    store.add("sha256:abc", "Jer", "inst-1")
    store.add("sha256:empty", "", "inst-1", client_label="client-host")
    entry = store.find_by_label("Jer")
    assert entry is not None
    assert entry.fingerprint == "sha256:abc"
    assert entry.role == ""
    assert store.find_by_label("Nope") is None
    assert store.find_by_label("") is None
    assert store.find_by_label("client-host") is None

    time.sleep(0.02)
    path.write_text(
        json.dumps(
            [
                {
                    "fingerprint": "sha256:xyz",
                    "device_label": "External",
                    "paired_at": "2026-04-19T00:00:00Z",
                    "instance_id": "inst-2",
                    "last_seen_at": "2026-04-19T18:03:12Z",
                }
            ],
            indent=2,
        )
        + "\n",
        encoding="utf-8",
    )

    reloaded = store.find_by_label("External")
    assert reloaded is not None
    assert reloaded.fingerprint == "sha256:xyz"
    assert reloaded.role == ""
    assert reloaded.last_seen_at == "2026-04-19T18:03:12Z"
    assert store.find_by_label("Jer") is None


def _load_payload(path: Path) -> list[dict[str, str]]:
    return json.loads(path.read_text(encoding="utf-8"))
