import pytest

from app.integrations.base import InvalidTokenError, ProductUnavailableError
from app.integrations.helixtahr.adapter import ALLOWED_PATH
from tests.fakes import MISSING_ROLE, FakeClock, FakeHelixtaHR, make_helixtahr_adapter
from tests.tokens import meeting_token


@pytest.fixture
def fake() -> FakeHelixtaHR:
    return FakeHelixtaHR()


@pytest.fixture
def clock() -> FakeClock:
    return FakeClock()


@pytest.fixture
def adapter(fake, clock):
    return make_helixtahr_adapter(fake, clock)


def _caller(adapter, **claims):
    token = meeting_token(**claims)
    return adapter.verify_token(token), token


async def _enabled(adapter, principal, token, **kwargs) -> bool:
    return (await adapter.check_access(principal, token, **kwargs)).enabled


@pytest.mark.parametrize(
    ("role_name", "is_org_admin"),
    [("admin", True), ("SuperAdmin", True), ("manager", False), ("", False)],
)
async def test_current_role_comes_from_the_answer(adapter, fake, role_name, is_org_admin):
    fake.role_name = role_name
    principal, token = _caller(adapter, role_name="manager")

    answer = await adapter.check_access(principal, token)

    assert answer.is_org_admin is is_org_admin


@pytest.mark.parametrize("role_name", [MISSING_ROLE, None, 5])
async def test_no_usable_role_in_the_answer_means_unknown(adapter, fake, role_name):
    fake.role_name = role_name
    principal, token = _caller(adapter)

    assert (await adapter.check_access(principal, token)).is_org_admin is None


async def test_reused_answer_reports_no_role(adapter, fake):
    fake.role_name = "admin"
    principal, token = _caller(adapter)
    await adapter.check_access(principal, token)

    # Another user of the same company: the company answer is reused, the role is not.
    other, other_token = _caller(adapter, sub="999")
    answer = await adapter.check_access(other, other_token)

    assert answer.enabled is True
    assert answer.is_org_admin is None
    assert fake.allowed_calls == 1


async def test_allowed_company(adapter, fake):
    principal, token = _caller(adapter)

    assert await _enabled(adapter, principal, token) is True
    request = fake.requests[0]
    assert request.method == "GET"
    assert request.url.path == ALLOWED_PATH
    assert request.headers["Authorization"] == f"Bearer {token}"


async def test_company_without_meetings(adapter, fake):
    fake.meetings_allowed = False
    principal, token = _caller(adapter)

    assert await _enabled(adapter, principal, token) is False


async def test_answer_is_reused_for_60_seconds(adapter, fake, clock):
    principal, token = _caller(adapter)
    await adapter.check_access(principal, token)

    fake.meetings_allowed = False
    clock.now += 59
    assert await _enabled(adapter, principal, token) is True
    assert fake.allowed_calls == 1

    clock.now += 1
    assert await _enabled(adapter, principal, token) is False
    assert fake.allowed_calls == 2


async def test_answer_is_kept_per_organisation(adapter, fake):
    principal_26, token_26 = _caller(adapter, company_id=26)
    principal_30, token_30 = _caller(adapter, company_id=30)
    await adapter.check_access(principal_26, token_26)

    fake.meetings_allowed = False
    assert await _enabled(adapter, principal_30, token_30) is False
    assert await _enabled(adapter, principal_26, token_26) is True


async def test_fresh_always_asks_again(adapter, fake):
    principal, token = _caller(adapter)
    await adapter.check_access(principal, token)

    fake.meetings_allowed = False
    assert await _enabled(adapter, principal, token, fresh=True) is False
    assert fake.allowed_calls == 2
    # The fresh answer replaces the remembered one.
    assert await _enabled(adapter, principal, token) is False


@pytest.mark.parametrize("status_code", [401, 403])
async def test_token_refused_by_helixtahr(adapter, fake, status_code):
    fake.status_code = status_code
    principal, token = _caller(adapter)

    with pytest.raises(InvalidTokenError):
        await adapter.check_access(principal, token)


async def test_helixtahr_unreachable(adapter, fake):
    fake.unreachable = True
    principal, token = _caller(adapter)

    with pytest.raises(ProductUnavailableError):
        await adapter.check_access(principal, token)


@pytest.mark.parametrize("status_code", [404, 500, 502])
async def test_helixtahr_error_status(adapter, fake, status_code):
    fake.status_code = status_code
    principal, token = _caller(adapter)

    with pytest.raises(ProductUnavailableError):
        await adapter.check_access(principal, token)


@pytest.mark.parametrize("value", [None, "true", 1, "yes"])
async def test_unexpected_answer_is_unavailable_not_a_yes(adapter, fake, value):
    fake.meetings_allowed = value
    principal, token = _caller(adapter)

    with pytest.raises(ProductUnavailableError):
        await adapter.check_access(principal, token)


@pytest.mark.parametrize("body", [b"<html>error</html>", b"[true]", b""])
async def test_non_object_answer_is_unavailable(adapter, fake, body):
    fake.raw_body = body
    principal, token = _caller(adapter)

    with pytest.raises(ProductUnavailableError):
        await adapter.check_access(principal, token)


async def test_failures_are_not_remembered(adapter, fake):
    fake.unreachable = True
    principal, token = _caller(adapter)
    with pytest.raises(ProductUnavailableError):
        await adapter.check_access(principal, token)

    fake.unreachable = False
    assert await _enabled(adapter, principal, token) is True
