import pytest
from typing import cast

from frisk_sdk.errors.configuration_errors import TokenRuntimeConfigurationConflictError


class FakeAccessTokenProvider:
    """Deterministic stand-in for AccessTokenProvider used by AccessTokenProviderRuntime tests."""

    def __init__(
        self,
        api_key: str,
        *,
        refresh_interval_minutes: int,
        base_url: str,
        callbacks,
        parent_logger_name=None,
    ):
        self.api_key = api_key
        self.refresh_interval_minutes = refresh_interval_minutes
        self.base_url = base_url
        self.callbacks = list(callbacks)
        self.parent_logger_name = parent_logger_name

        self.started = False
        self.stopped = False
        self._token = "tok-0"

    def start_background_refresh(self) -> None:
        self.started = True

    def stop_background_refresh(self) -> None:
        self.stopped = True

    def get_access_token(self) -> str:
        return self._token

    def emit_token(self, token: str) -> None:
        """Manually simulate a token update from the underlying AccessTokenProvider."""
        self._token = token
        for cb in list(self.callbacks):
            cb(token)


@pytest.fixture(autouse=True)
def _clean_global_runtime():
    """Ensure each test starts with a clean singleton runtime."""
    from frisk_sdk.core.access_token_provider_runtime import (
        reset_token_runtime_for_testing,
    )

    reset_token_runtime_for_testing()
    yield
    reset_token_runtime_for_testing()


@pytest.fixture
def _fake_access_token_provider(monkeypatch: pytest.MonkeyPatch) -> None:
    """Patch AccessTokenProviderRuntime module to use FakeAccessTokenProvider."""

    import frisk_sdk.core.access_token_provider_runtime as tmr

    monkeypatch.setattr(tmr, "AccessTokenProvider", FakeAccessTokenProvider)


@pytest.mark.unit
def test_get_or_init_token_runtime_returns_singleton(_fake_access_token_provider):
    from frisk_sdk.core.access_token_provider_runtime import (
        get_or_init_access_token_provider_runtime,
    )

    rt1 = get_or_init_access_token_provider_runtime(
        api_key="k1", base_url="http://example", refresh_interval_minutes=5
    )
    rt2 = get_or_init_access_token_provider_runtime(
        api_key="k1", base_url="http://example", refresh_interval_minutes=5
    )

    assert rt1 is rt2
    fake_tm = cast(FakeAccessTokenProvider, rt1._tm)
    assert fake_tm.started is True


@pytest.mark.unit
def test_get_or_init_token_runtime_raises_on_conflicting_config(
    _fake_access_token_provider,
):
    from frisk_sdk.core.access_token_provider_runtime import (
        get_or_init_access_token_provider_runtime,
    )

    get_or_init_access_token_provider_runtime(
        api_key="k1", base_url="http://example", refresh_interval_minutes=5
    )

    with pytest.raises(BaseException) as e:
        get_or_init_access_token_provider_runtime(
            api_key="k2", base_url="http://example", refresh_interval_minutes=5
        )

    assert isinstance(e.value, TokenRuntimeConfigurationConflictError)


@pytest.mark.unit
def test_subscribe_dispatches_to_all_subscribers_and_ignores_exceptions(
    _fake_access_token_provider,
):
    from frisk_sdk.core.access_token_provider_runtime import (
        get_or_init_access_token_provider_runtime,
    )

    rt = get_or_init_access_token_provider_runtime(
        api_key="k1", base_url="http://example", refresh_interval_minutes=5
    )

    received_1: list[str] = []
    received_2: list[str] = []

    def cb1(tok: str) -> None:
        received_1.append(tok)

    def cb2(tok: str) -> None:
        received_2.append(tok)
        raise RuntimeError("subscriber should not break dispatch")

    rt.subscribe(cb1)
    rt.subscribe(cb2)

    cast(FakeAccessTokenProvider, rt._tm).emit_token("tok-1")

    assert received_1 == ["tok-1"]
    assert received_2 == ["tok-1"]


@pytest.mark.unit
def test_unsubscribe_stops_receiving_updates(_fake_access_token_provider):
    from frisk_sdk.core.access_token_provider_runtime import (
        get_or_init_access_token_provider_runtime,
    )

    rt = get_or_init_access_token_provider_runtime(
        api_key="k1", base_url="http://example", refresh_interval_minutes=5
    )

    received: list[str] = []

    def cb(tok: str) -> None:
        received.append(tok)

    rt.subscribe(cb)
    cast(FakeAccessTokenProvider, rt._tm).emit_token("tok-1")
    rt.unsubscribe(cb)
    cast(FakeAccessTokenProvider, rt._tm).emit_token("tok-2")

    assert received == ["tok-1"]


@pytest.mark.unit
def test_get_access_token_delegates_to_manager(_fake_access_token_provider):
    from frisk_sdk.core.access_token_provider_runtime import (
        get_or_init_access_token_provider_runtime,
    )

    rt = get_or_init_access_token_provider_runtime(
        api_key="k1", base_url="http://example", refresh_interval_minutes=5
    )

    cast(FakeAccessTokenProvider, rt._tm)._token = "tok-xyz"
    assert rt.get_access_token() == "tok-xyz"


@pytest.mark.unit
def test_reset_token_runtime_for_testing_stops_and_clears(_fake_access_token_provider):
    import frisk_sdk.core.access_token_provider_runtime as tmr

    rt = tmr.get_or_init_access_token_provider_runtime(
        api_key="k1", base_url="http://example", refresh_interval_minutes=5
    )

    # sanity
    assert rt is tmr.get_or_init_access_token_provider_runtime(
        api_key="k1", base_url="http://example", refresh_interval_minutes=5
    )

    tmr.reset_token_runtime_for_testing()

    # old AccessTokenProvider should have been stopped
    assert cast(FakeAccessTokenProvider, rt._tm).stopped is True

    # and singleton cleared
    rt2 = tmr.get_or_init_access_token_provider_runtime(
        api_key="k1", base_url="http://example", refresh_interval_minutes=5
    )
    assert rt2 is not rt


@pytest.mark.unit
def test_parent_logger_name_passed_through(_fake_access_token_provider):
    import frisk_sdk.core.access_token_provider_runtime as tmr

    rt = tmr.get_or_init_access_token_provider_runtime(
        api_key="k1",
        base_url="http://example",
        refresh_interval_minutes=5,
        parent_logger_name="my.logger",
    )

    assert cast(FakeAccessTokenProvider, rt._tm).parent_logger_name == "my.logger"
