# retoor <retoor@molodetz.nl>

from datetime import timedelta

from devplacepy.database import invalidate_admins_cache
from devplacepy.services.devii.tasks import limits
from devplacepy.services.devii.tasks.context import task_run_scope
from devplacepy.services.devii.tasks.guards import (
    REASON_BUDGET,
    REASON_CREATE_QUOTA,
    REASON_EXPIRED,
    REASON_FAILURES,
    REASON_MAX_RUNS,
    REASON_NESTED,
    REASON_NOT_A_USER,
    REASON_RUN_QUOTA,
    automation_allowed,
    budget_deferral,
    creation_denial,
    retire_reason,
    run_deferral,
)
from devplacepy.services.devii.tasks.limits import reserve_run
from devplacepy.services.devii.tasks.schedule import now_utc, to_iso
from devplacepy.utils import generate_uid

SIGNUP_AFTER_PRIMARY_ADMIN = "2099-01-01T00:00:00"


def _account(local_db, role):
    uid = generate_uid()
    local_db["users"].insert(
        {
            "uid": uid,
            "username": f"guard-{uid[-10:]}",
            "terms_version": "1",
            "role": role,
            "deleted_at": None,
            "created_at": SIGNUP_AFTER_PRIMARY_ADMIN,
        }
    )
    invalidate_admins_cache()
    return uid


def _row(owner_uid, **overrides):
    row = {
        "uid": generate_uid(),
        "owner_kind": "user",
        "owner_id": owner_uid,
        "run_count": 0,
        "created_at": to_iso(now_utc()),
    }
    row.update(overrides)
    return row


def _burn_runs(local_db, owner, count, reference):
    for index in range(count):
        limits.record_run(
            local_db, "user", owner, generate_uid(), reference - timedelta(minutes=index)
        )


def test_any_signed_in_account_may_automate(local_db):
    member = _account(local_db, "Member")
    admin = _account(local_db, "Admin")
    assert automation_allowed("user", member) is True
    assert automation_allowed("user", admin) is True
    assert automation_allowed("guest", "guest-cookie") is False
    assert automation_allowed("user", "") is False


def test_member_task_is_no_longer_retired_for_its_role(local_db):
    member = _account(local_db, "Member")
    assert retire_reason(_row(member), now_utc()) is None


def test_guest_owned_task_is_retired(local_db):
    row = _row("cookie", owner_kind="guest")
    assert retire_reason(row, now_utc()) == REASON_NOT_A_USER


def test_expired_task_is_retired(local_db):
    owner = _account(local_db, "Member")
    now = now_utc()
    row = _row(owner, expires_at=to_iso(now - timedelta(minutes=1)))
    assert retire_reason(row, now) == REASON_EXPIRED


def test_exhausted_task_is_retired(local_db):
    owner = _account(local_db, "Member")
    assert retire_reason(_row(owner, max_runs=5, run_count=5), now_utc()) == REASON_MAX_RUNS


def test_task_without_an_expiry_stops_at_the_fallback_ceiling(local_db):
    owner = _account(local_db, "Member")
    now = now_utc()
    row = _row(owner, created_at=to_iso(now - timedelta(days=40)))
    assert retire_reason(row, now) == REASON_EXPIRED


def test_repeated_failures_retire_the_task(local_db):
    owner = _account(local_db, "Member")
    row = _row(owner, failure_count=3)
    assert retire_reason(row, now_utc(), max_failures=3) == REASON_FAILURES
    assert retire_reason(row, now_utc(), max_failures=0) is None


def test_reservation_succeeds_until_the_quota_is_spent(local_db):
    owner = _account(local_db, "Member")
    now = now_utc()
    granted = [
        reserve_run(local_db, "user", owner, generate_uid(), now) for _ in range(12)
    ]
    assert granted.count(True) == 10
    assert granted.count(False) == 2


def test_a_spent_quota_defers_instead_of_retiring(local_db):
    owner = _account(local_db, "Member")
    now = now_utc()
    row = _row(owner)
    _burn_runs(local_db, owner, 10, now)
    assert reserve_run(local_db, "user", owner, row["uid"], now) is False
    postponement = run_deferral(local_db, row, now)
    assert postponement.reason == REASON_RUN_QUOTA
    assert postponement.retry_at > now
    assert retire_reason(row, now) is None


def test_administrator_run_quota_is_larger(local_db):
    owner = _account(local_db, "Admin")
    now = now_utc()
    _burn_runs(local_db, owner, 10, now)
    assert reserve_run(local_db, "user", owner, generate_uid(), now) is True


def test_budget_defers_the_task(local_db):
    owner = _account(local_db, "Member")
    now = now_utc()
    postponement = budget_deferral(_row(owner), now, budget_exceeded=lambda k, u: True)
    assert postponement is not None
    assert postponement.reason == REASON_BUDGET
    assert postponement.retry_at > now
    assert budget_deferral(_row(owner), now, lambda k, u: False) is None
    assert budget_deferral(_row(owner), now, None) is None


def test_creation_is_allowed_within_the_quota(local_db):
    owner = _account(local_db, "Member")
    assert creation_denial(local_db, "user", owner, now_utc()) is None


def test_creation_is_denied_over_the_quota(local_db):
    owner = _account(local_db, "Member")
    now = now_utc()
    for _ in range(5):
        local_db["devii_tasks"].insert(
            {
                "uid": generate_uid(),
                "owner_kind": "user",
                "owner_id": owner,
                "created_at": to_iso(now),
                "deleted_at": None,
            }
        )
    denial = creation_denial(local_db, "user", owner, now)
    assert denial is not None
    assert denial.reason == REASON_CREATE_QUOTA
    assert denial.retry_at is not None


def test_creation_inside_a_task_run_is_denied_for_a_member(local_db):
    owner = _account(local_db, "Member")
    with task_run_scope():
        denial = creation_denial(local_db, "user", owner, now_utc())
    assert denial is not None
    assert denial.reason == REASON_NESTED


def test_creation_inside_a_task_run_is_allowed_for_an_administrator(local_db):
    owner = _account(local_db, "Admin")
    with task_run_scope():
        assert creation_denial(local_db, "user", owner, now_utc()) is None


def test_guest_creation_is_denied(local_db):
    denial = creation_denial(local_db, "guest", "cookie", now_utc())
    assert denial is not None
    assert denial.reason == REASON_NOT_A_USER
