# retoor <retoor@molodetz.nl>

import secrets
from datetime import datetime, timedelta, timezone

import pytest
from fastapi.testclient import TestClient

from devplacepy.database import get_table
from devplacepy.utils import generate_uid, _resolve_user, hash_password
from devplacepy.services.access_tokens import (
    issue_token,
    resolve_token,
    revoke_token,
    revoke_all,
    prune_expired,
)
from devplacepy.main import app


# ── mock helpers ─────────────────────────────────────────────────────────────

class _MockHeaders:
    def __init__(self, data: dict):
        self._data = data

    def get(self, key: str, default=None):
        return self._data.get(key, default)


class _MockCookies:
    def __init__(self, data: dict):
        self._data = data

    def get(self, key: str):
        return self._data.get(key)


class _MockRequest:
    def __init__(self, headers=None, cookies=None):
        self.headers = _MockHeaders(headers or {})
        self.cookies = _MockCookies(cookies or {})


# ── seed helpers ─────────────────────────────────────────────────────────────

def _seed_user(username, email, is_active=True, **extra):
    uid = generate_uid()
    get_table("users").insert(
        {
            "uid": uid,
            "username": username,
            "terms_version": "1",
            "email": email,
            "is_active": is_active,
            "created_at": datetime.now(timezone.utc).isoformat(),
            **extra,
        }
    )
    return uid


# ── service tests ────────────────────────────────────────────────────────────

def test_issue_token_returns_expected_fields(local_db):
    """issue_token returns all expected fields with correct types/values."""
    uid = _seed_user("issuer", "issuer@test.dev")
    user = get_table("users").find_one(uid=uid)

    result = issue_token(user)

    assert isinstance(result, dict)
    assert "access_token" in result
    assert len(result["access_token"]) == 64  # 32 bytes hex
    assert result["token_type"] == "bearer"
    assert isinstance(result["expires_in"], int)
    assert result["expires_in"] > 0
    assert "expires_at" in result
    assert "uid" in result
    assert len(result["uid"]) > 0


def test_resolve_token_valid(local_db):
    """Issue a token then resolve it - returns the correct user."""
    uid = _seed_user("resolver", "resolver@test.dev")
    user = get_table("users").find_one(uid=uid)

    result = issue_token(user)
    resolved = resolve_token(result["access_token"])

    assert resolved is not None
    assert resolved["uid"] == uid
    assert resolved["username"] == "resolver"


def test_resolve_token_not_found(local_db):
    """A random 64-char hex string that has no matching token returns None."""
    random_token = secrets.token_hex(32)
    assert resolve_token(random_token) is None


def test_resolve_token_expired(local_db):
    """Issue a token with max_age_days=0 (expires immediately) → resolve returns None."""
    uid = _seed_user("expired_tok", "expiredtok@test.dev")
    user = get_table("users").find_one(uid=uid)

    result = issue_token(user, max_age_days=0)
    resolved = resolve_token(result["access_token"])

    assert resolved is None


def test_resolve_token_inactive_user(local_db):
    """Issue a token for an inactive user → resolve returns None."""
    uid = _seed_user("inactive_tok", "inactivetok@test.dev", is_active=False)
    user = get_table("users").find_one(uid=uid)

    result = issue_token(user)
    resolved = resolve_token(result["access_token"])

    assert resolved is None


def test_revoke_token_valid(local_db):
    """Issue a token, revoke by uid, resolve returns None."""
    uid = _seed_user("revoker", "revoker@test.dev")
    user = get_table("users").find_one(uid=uid)

    result = issue_token(user)
    token_uid = result["uid"]
    token_str = result["access_token"]

    # Token resolves before revoke
    assert resolve_token(token_str) is not None

    revoked = revoke_token(token_uid)
    assert revoked is True

    # Token no longer resolves
    assert resolve_token(token_str) is None


def test_revoke_token_not_found(local_db):
    """Revoking a random uid returns False."""
    random_uid = generate_uid()
    assert revoke_token(random_uid) is False


def test_revoke_all(local_db):
    """Issue 3 tokens, revoke_all, all resolve to None."""
    uid = _seed_user("revokeall", "revokeall@test.dev")
    user = get_table("users").find_one(uid=uid)

    tokens = [issue_token(user) for _ in range(3)]
    for t in tokens:
        assert resolve_token(t["access_token"]) is not None

    count = revoke_all(uid)
    assert count == 3

    for t in tokens:
        assert resolve_token(t["access_token"]) is None


def test_prune_expired(local_db):
    """Issue expired tokens, prune, all resolve to None."""
    uid = _seed_user("pruner", "pruner@test.dev")
    user = get_table("users").find_one(uid=uid)

    # max_age_days=-1 → expires_at is 1 day in the past
    tokens = [issue_token(user, max_age_days=-1) for _ in range(2)]

    # They're already expired, resolve returns None without pruning
    for t in tokens:
        assert resolve_token(t["access_token"]) is None

    # But they still exist in the table (deleted_at is None)
    for t in tokens:
        row = get_table("access_tokens").find_one(uid=t["uid"])
        assert row is not None
        assert row["deleted_at"] is None

    pruned = prune_expired()
    assert pruned >= 2

    # Now they're soft-deleted
    for t in tokens:
        row = get_table("access_tokens").find_one(uid=t["uid"])
        assert row["deleted_at"] is not None


def test_issue_token_with_label(local_db):
    """Label is stored in the access_tokens table."""
    uid = _seed_user("labeler", "labeler@test.dev")
    user = get_table("users").find_one(uid=uid)

    result = issue_token(user, label="my-cli-token")
    row = get_table("access_tokens").find_one(uid=result["uid"])

    assert row is not None
    assert row["label"] == "my-cli-token"


# ── auth chain integration tests ─────────────────────────────────────────────

def test_resolve_user_from_access_token_x_api_key(local_db):
    """_resolve_user finds a user from X-API-KEY header containing an access token."""
    uid = _seed_user("acctok_xapikey", "acctokxapikey@test.dev")
    user = get_table("users").find_one(uid=uid)

    result = issue_token(user)
    token_str = result["access_token"]

    request = _MockRequest(headers={"X-API-KEY": token_str})
    resolved = _resolve_user(request)

    assert resolved is not None
    assert resolved["uid"] == uid
    assert resolved["username"] == "acctok_xapikey"


def test_resolve_user_from_access_token_bearer(local_db):
    """_resolve_user finds a user from Authorization: Bearer with access token."""
    uid = _seed_user("acctok_bearer", "acctokbearer@test.dev")
    user = get_table("users").find_one(uid=uid)

    result = issue_token(user)
    token_str = result["access_token"]

    request = _MockRequest(headers={"Authorization": f"Bearer {token_str}"})
    resolved = _resolve_user(request)

    assert resolved is not None
    assert resolved["uid"] == uid
    assert resolved["username"] == "acctok_bearer"


def test_resolve_user_access_token_priority_after_api_key(local_db):
    """api_key user wins over access token when the same string matches both."""
    shared_key = secrets.token_hex(32)  # 64-char hex

    # User A - owns the api_key
    uid_a = _seed_user("apikey_winner", "apikeywinner@test.dev", api_key=shared_key)

    # User B - owns an access token with the same string as the key
    uid_b = _seed_user("acctok_loser", "acctokloser@test.dev")
    get_table("access_tokens").insert(
        {
            "uid": generate_uid(),
            "token": shared_key,
            "user_uid": uid_b,
            "label": "",
            "expires_at": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat(),
            "created_at": datetime.now(timezone.utc).isoformat(),
            "deleted_at": None,
            "deleted_by": None,
        }
    )

    # X-API-KEY path - api_key user wins
    request = _MockRequest(headers={"X-API-KEY": shared_key})
    resolved = _resolve_user(request)
    assert resolved is not None
    assert resolved["uid"] == uid_a
    assert resolved["username"] == "apikey_winner"

    # Bearer path - api_key user wins
    request2 = _MockRequest(headers={"Authorization": f"Bearer {shared_key}"})
    resolved2 = _resolve_user(request2)
    assert resolved2 is not None
    assert resolved2["uid"] == uid_a
    assert resolved2["username"] == "apikey_winner"


def test_access_token_wrong_length_skipped(local_db):
    """Non-64-char strings never reach the access_tokens DB lookup."""
    # Empty / falsy
    request = _MockRequest(headers={"X-API-KEY": ""})
    assert _resolve_user(request) is None

    # Too short
    request = _MockRequest(headers={"X-API-KEY": "abc123"})
    assert _resolve_user(request) is None

    # 63 chars (one short)
    request = _MockRequest(headers={"X-API-KEY": "a" * 63})
    assert _resolve_user(request) is None

    # 65 chars (one too many)
    request = _MockRequest(headers={"X-API-KEY": "a" * 65})
    assert _resolve_user(request) is None

    # 40-char hex reaches devrant token lookup (not access token)
    # but with no DB match it returns None
    request = _MockRequest(headers={"X-API-KEY": "a" * 40})
    assert _resolve_user(request) is None


# ── endpoint tests (TestClient) ──────────────────────────────────────────────

@pytest.fixture
def _token_test_user(local_db):
    """Seed a user with a known password for the token endpoint tests."""
    pw_hash = hash_password("test123456")
    uid = _seed_user("tokentest", "tokentest@test.dev", password_hash=pw_hash)
    return uid


def test_token_endpoint_valid_credentials(local_db, _token_test_user):
    """POST /auth/token with valid email+password → 200 with access_token."""
    client = TestClient(app)
    resp = client.post(
        "/auth/token",
        data={"email": "tokentest@test.dev", "password": "test123456"},
    )

    assert resp.status_code == 200
    body = resp.json()
    assert "access_token" in body
    assert len(body["access_token"]) == 64
    assert body["token_type"] == "bearer"
    assert isinstance(body["expires_in"], int)
    assert body["expires_in"] > 0


def test_token_endpoint_valid_username(local_db, _token_test_user):
    """POST /auth/token with username instead of email → 200."""
    client = TestClient(app)
    resp = client.post(
        "/auth/token",
        data={"email": "tokentest", "password": "test123456"},
    )

    assert resp.status_code == 200
    body = resp.json()
    assert "access_token" in body


def test_token_endpoint_bad_password(local_db, _token_test_user):
    """POST /auth/token with wrong password → 401."""
    client = TestClient(app)
    resp = client.post(
        "/auth/token",
        data={"email": "tokentest@test.dev", "password": "wrongpassword"},
    )

    assert resp.status_code == 401
    body = resp.json()
    assert "error" in body


def test_token_endpoint_missing_fields(local_db):
    """POST /auth/token with whitespace-only fields → 400.

    Pydantic rejects truly missing/empty fields (422), but the endpoint's
    own guard catches values that pass Pydantic's min_length yet become
    empty after strip(), e.g. a single space.
    """
    client = TestClient(app)

    # Whitespace-only email passes Pydantic (len >= 1), stripped to empty → 400
    resp = client.post(
        "/auth/token",
        json={"email": " ", "password": "secret"},
    )
    assert resp.status_code == 400
    assert resp.json()["error"] == "email and password are required"

    # Both whitespace-only → 400
    resp = client.post(
        "/auth/token",
        json={"email": " ", "password": " "},
    )
    assert resp.status_code == 400
    assert resp.json()["error"] == "email and password are required"
