from __future__ import annotations

import pytest

from src.services import awooop_approval_token as approval_token


class FakeRedis:
    def __init__(self) -> None:
        self.values: dict[str, str] = {}
        self.sets: dict[str, set[str]] = {}
        self.expirations: dict[str, int] = {}

    async def set(
        self,
        key: str,
        value: str,
        *,
        nx: bool = False,
        ex: int | None = None,
    ) -> bool:
        if nx and key in self.values:
            return False
        self.values[key] = value
        if ex is not None:
            self.expirations[key] = ex
        return True

    async def sadd(self, key: str, member: str) -> int:
        members = self.sets.setdefault(key, set())
        before = len(members)
        members.add(member)
        return 1 if len(members) > before else 0

    async def expire(self, key: str, ttl: int) -> bool:
        self.expirations[key] = ttl
        return True

    async def scard(self, key: str) -> int:
        return len(self.sets.get(key, set()))


@pytest.fixture(autouse=True)
def stable_hmac(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(approval_token, "_get_hmac_key", lambda: b"test-hmac-key")


@pytest.mark.asyncio
async def test_record_approval_uses_shared_redis_pool(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    redis = FakeRedis()
    monkeypatch.setattr(approval_token, "get_redis", lambda: redis)

    token = approval_token.issue_approval_token(
        project_id="awoooi",
        run_id="run-1",
        tool_name="operator_console_approve",
        approver_id="telegram:123456",
    )

    count = await approval_token.record_approval(
        project_id="awoooi",
        run_id="run-1",
        tool_name="operator_console_approve",
        approver_id="telegram:123456",
        token=token,
    )

    assert count == 1
    assert any(key.startswith("awooop_appr:jti:") for key in redis.values)
    assert redis.sets[
        "awooop_appr:sigs:awoooi:run-1:operator_console_approve"
    ] == {"telegram:123456"}


@pytest.mark.asyncio
async def test_record_approval_rejects_token_replay(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    redis = FakeRedis()
    monkeypatch.setattr(approval_token, "get_redis", lambda: redis)

    token = approval_token.issue_approval_token(
        project_id="awoooi",
        run_id="run-1",
        tool_name="operator_console_approve",
        approver_id="telegram:123456",
    )

    await approval_token.record_approval(
        project_id="awoooi",
        run_id="run-1",
        tool_name="operator_console_approve",
        approver_id="telegram:123456",
        token=token,
    )

    with pytest.raises(approval_token.TokenReplayError):
        await approval_token.record_approval(
            project_id="awoooi",
            run_id="run-1",
            tool_name="operator_console_approve",
            approver_id="telegram:123456",
            token=token,
        )


@pytest.mark.asyncio
async def test_check_approval_quorum_uses_shared_redis_pool(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    redis = FakeRedis()
    redis.sets["awooop_appr:sigs:awoooi:run-1:tool"] = {"operator-a"}
    monkeypatch.setattr(approval_token, "get_redis", lambda: redis)

    assert await approval_token.check_approval_quorum(
        project_id="awoooi",
        run_id="run-1",
        tool_name="tool",
        required_count=1,
    )


@pytest.mark.asyncio
async def test_check_approval_quorum_rejects_insufficient_count(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    redis = FakeRedis()
    monkeypatch.setattr(approval_token, "get_redis", lambda: redis)

    with pytest.raises(approval_token.QuorumNotMetError):
        await approval_token.check_approval_quorum(
            project_id="awoooi",
            run_id="run-1",
            tool_name="tool",
            required_count=1,
        )
