from __future__ import annotations

from contextlib import contextmanager
from types import SimpleNamespace
from typing import Any

from flask import Flask

from atlas_portal.routes.access_request_onboarding import register_access_request_onboarding


class DummyResult:
    def __init__(self, row: dict[str, Any] | None = None) -> None:
        self.row = row

    def fetchone(self) -> dict[str, Any] | None:
        return self.row


class DummyConn:
    def __init__(self, row: dict[str, Any] | None = None, *, fail: bool = False) -> None:
        self.row = row
        self.fail = fail
        self.executed: list[tuple[str, object | None]] = []

    def execute(self, query: str, params: object | None = None) -> DummyResult:
        self.executed.append((query, params))
        if self.fail:
            raise RuntimeError("database failed")
        return DummyResult(self.row)


class DummyOidc:
    def __init__(self, *, fail: bool = False, claims: dict[str, Any] | None = None) -> None:
        self.fail = fail
        self.claims = claims or {"preferred_username": "alice", "groups": ["/vaultwarden_grandfathered"]}

    def verify(self, token: str) -> dict[str, Any]:
        if self.fail:
            raise RuntimeError("bad token")
        return self.claims


class DummyAdmin:
    def __init__(
        self,
        *,
        ready: bool = True,
        user: dict[str, Any] | None = None,
        full: dict[str, Any] | None = None,
        fail_attrs: bool = False,
    ) -> None:
        self._ready = ready
        self.user = user if user is not None else {"id": "user-1"}
        self.full = full if full is not None else {"requiredActions": []}
        self.fail_attrs = fail_attrs
        self.attributes: list[tuple[str, str, str]] = []
        self.updated: list[tuple[str, dict[str, Any]]] = []

    def ready(self) -> bool:
        return self._ready

    def set_user_attribute(self, username: str, key: str, value: str) -> None:
        if self.fail_attrs:
            raise RuntimeError("attribute update failed")
        self.attributes.append((username, key, value))

    def find_user(self, username: str) -> dict[str, Any] | None:
        return self.user

    def get_user(self, user_id: str) -> dict[str, Any]:
        return self.full

    def update_user_safe(self, user_id: str, payload: dict[str, Any]) -> None:
        self.updated.append((user_id, payload))


class DummyDeps:
    ONBOARDING_STEPS = {
        "profile_reviewed",
        "vaultwarden_master_password",
        "vaultwarden_store_temp_password",
        "keycloak_password_rotated",
    }
    KEYCLOAK_MANAGED_STEPS = {"keycloak_password_rotated"}
    ONBOARDING_STEP_PREREQUISITES = {
        "vaultwarden_master_password": {"profile_reviewed"},
        "keycloak_password_rotated": {"profile_reviewed"},
    }
    VAULTWARDEN_GRANDFATHERED_FLAG = "vaultwarden_grandfathered"
    _KEYCLOAK_PASSWORD_ROTATION_REQUESTED_ARTIFACT = "keycloak_password_rotation_requested"

    def __init__(self, conn: DummyConn | None = None) -> None:
        self.configured_value = True
        self.conn = conn or DummyConn(self.request_row())
        self.oidc = DummyOidc()
        self.admin = DummyAdmin()
        self.completed_steps: set[str] = {"profile_reviewed"}
        self.rotation_requested = True
        self.request_rotation_fails = False
        self.user_in_group = False
        self.recovery_email = "alice@example.dev"
        self.advanced_status = "ready"

    def request_row(self, **overrides: Any) -> dict[str, Any]:
        row = {
            "username": "alice",
            "status": "awaiting_onboarding",
            "approval_flags": [],
            "contact_email": "alice@example.dev",
        }
        row.update(overrides)
        return row

    def configured(self) -> bool:
        return self.configured_value

    @contextmanager
    def connect(self):
        yield self.conn

    def oidc_client(self) -> DummyOidc:
        return self.oidc

    def admin_client(self) -> DummyAdmin:
        return self.admin

    def _normalize_status(self, status: str) -> str:
        return "accounts_building" if status == "approved" else (status or "unknown")

    def _normalize_flag_list(self, raw: Any) -> set[str]:
        return {item for item in raw if isinstance(item, str)} if isinstance(raw, list) else set()

    def _completed_onboarding_steps(self, conn: DummyConn, code: str, username: str) -> set[str]:
        return self.completed_steps

    def _password_rotation_requested(self, conn: DummyConn, code: str) -> bool:
        return self.rotation_requested

    def _request_keycloak_password_rotation(self, conn: DummyConn, code: str, username: str) -> None:
        if self.request_rotation_fails:
            raise RuntimeError("rotation request failed")
        self.rotation_requested = True

    def _user_in_group(self, username: str, group: str) -> bool:
        return self.user_in_group

    def _resolve_recovery_email(self, username: str, fallback: str) -> str:
        return self.recovery_email or fallback

    def _advance_status(self, conn: DummyConn, code: str, username: str, status: str) -> str:
        return self.advanced_status

    def _onboarding_payload(self, conn: DummyConn, code: str, username: str) -> dict[str, str]:
        return {"code": code, "username": username}


def make_client(deps: DummyDeps):
    app = Flask(__name__)
    register_access_request_onboarding(app, deps)
    return app.test_client()


def test_attest_preflight_token_and_lookup_paths() -> None:
    deps = DummyDeps()
    client = make_client(deps)

    deps.configured_value = False
    assert client.post("/api/access/request/onboarding/attest", json={"code": "code", "step": "profile_reviewed"}).status_code == 503
    deps.configured_value = True

    assert client.post("/api/access/request/onboarding/attest", json={"step": "profile_reviewed"}).status_code == 400
    assert client.post("/api/access/request/onboarding/attest", json={"code": "code", "step": "missing"}).status_code == 400
    assert client.post("/api/access/request/onboarding/attest", json={"code": "code", "step": "keycloak_password_rotated"}).status_code == 400
    assert client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "profile_reviewed"},
        headers={"Authorization": "bad"},
    ).status_code == 401
    assert client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "profile_reviewed"},
        headers={"Authorization": "Bearer "},
    ).status_code == 401
    deps.oidc = DummyOidc(fail=True)
    assert client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "profile_reviewed"},
        headers={"Authorization": "Bearer token"},
    ).status_code == 401
    deps.oidc = DummyOidc(claims={"preferred_username": "other", "groups": []})
    assert client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "profile_reviewed"},
        headers={"Authorization": "Bearer token"},
    ).status_code == 403

    deps.oidc = DummyOidc()
    deps.conn = DummyConn(None)
    assert client.post("/api/access/request/onboarding/attest", json={"code": "missing", "step": "profile_reviewed"}).status_code == 404

    deps.conn = DummyConn(deps.request_row(status="pending"))
    assert client.post("/api/access/request/onboarding/attest", json={"code": "code", "step": "profile_reviewed"}).status_code == 409


def test_attest_prerequisites_rotation_and_manual_clear_paths() -> None:
    deps = DummyDeps()
    client = make_client(deps)

    deps.completed_steps = set()
    response = client.post("/api/access/request/onboarding/attest", json={"code": "code", "step": "vaultwarden_master_password"})
    assert response.status_code == 409
    assert response.get_json()["blocked_by"] == ["profile_reviewed"]

    deps.completed_steps = {"profile_reviewed"}
    deps.rotation_requested = False
    deps.request_rotation_fails = True
    assert client.post("/api/access/request/onboarding/attest", json={"code": "code", "step": "vaultwarden_store_temp_password"}).status_code == 502

    deps.request_rotation_fails = False
    response = client.post("/api/access/request/onboarding/attest", json={"code": "code", "step": "profile_reviewed", "completed": False})
    assert response.status_code == 200
    assert any("DELETE FROM access_request_onboarding_steps" in query for query, _ in deps.conn.executed)

    deps.conn = DummyConn(deps.request_row(), fail=True)
    assert client.post("/api/access/request/onboarding/attest", json={"code": "code", "step": "profile_reviewed"}).status_code == 502


def test_attest_vaultwarden_claim_and_attribute_paths() -> None:
    deps = DummyDeps()
    client = make_client(deps)

    assert client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "vaultwarden_master_password", "vaultwarden_claim": True},
    ).status_code == 401

    deps.oidc = DummyOidc(claims={"preferred_username": "alice", "groups": []})
    deps.completed_steps = {"profile_reviewed"}
    deps.user_in_group = False
    assert client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "vaultwarden_master_password", "vaultwarden_claim": True},
        headers={"Authorization": "Bearer token"},
    ).status_code == 403

    deps.conn = DummyConn(deps.request_row(approval_flags=[deps.VAULTWARDEN_GRANDFATHERED_FLAG]))
    deps.admin = DummyAdmin(ready=False)
    assert client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "vaultwarden_master_password", "vaultwarden_claim": True},
        headers={"Authorization": "Bearer token"},
    ).status_code == 503

    deps.admin = DummyAdmin()
    deps.recovery_email = ""
    assert client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "vaultwarden_master_password", "vaultwarden_claim": True},
        headers={"Authorization": "Bearer token"},
    ).status_code == 200

    deps.recovery_email = "recovery@example.dev"
    response = client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "vaultwarden_master_password", "vaultwarden_claim": True},
        headers={"Authorization": "Bearer token"},
    )
    assert response.status_code == 200
    assert ("alice", "vaultwarden_email", "recovery@example.dev") in deps.admin.attributes
    assert any("INSERT INTO access_request_onboarding_steps" in query for query, _ in deps.conn.executed)

    deps.admin = DummyAdmin()
    response = client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "vaultwarden_master_password"},
    )
    assert response.status_code == 200
    assert ("alice", "vaultwarden_status", "already_present") in deps.admin.attributes

    deps.admin = DummyAdmin(fail_attrs=True)
    assert client.post(
        "/api/access/request/onboarding/attest",
        json={"code": "code", "step": "vaultwarden_master_password"},
    ).status_code == 502


def test_keycloak_rotate_preflight_and_lookup_paths() -> None:
    deps = DummyDeps()
    client = make_client(deps)

    deps.configured_value = False
    assert client.post("/api/access/request/onboarding/keycloak-password-rotate", json={"code": "code"}).status_code == 503
    deps.configured_value = True

    assert client.post("/api/access/request/onboarding/keycloak-password-rotate", json={}).status_code == 400
    assert client.post(
        "/api/access/request/onboarding/keycloak-password-rotate",
        json={"code": "code"},
        headers={"Authorization": "bad"},
    ).status_code == 401
    deps.oidc = DummyOidc(fail=True)
    assert client.post(
        "/api/access/request/onboarding/keycloak-password-rotate",
        json={"code": "code"},
        headers={"Authorization": "Bearer token"},
    ).status_code == 401
    deps.oidc = DummyOidc()

    deps.admin = DummyAdmin(ready=False)
    assert client.post("/api/access/request/onboarding/keycloak-password-rotate", json={"code": "code"}).status_code == 503
    deps.admin = DummyAdmin()

    deps.conn = DummyConn(None)
    assert client.post("/api/access/request/onboarding/keycloak-password-rotate", json={"code": "missing"}).status_code == 404

    deps.conn = DummyConn(deps.request_row())
    deps.oidc = DummyOidc(claims={"preferred_username": "other"})
    assert client.post(
        "/api/access/request/onboarding/keycloak-password-rotate",
        json={"code": "code"},
        headers={"Authorization": "Bearer token"},
    ).status_code == 403

    deps.oidc = DummyOidc()
    deps.conn = DummyConn(deps.request_row(status="pending"))
    assert client.post("/api/access/request/onboarding/keycloak-password-rotate", json={"code": "code"}).status_code == 409

    deps.conn = DummyConn(deps.request_row())
    deps.completed_steps = set()
    assert client.post("/api/access/request/onboarding/keycloak-password-rotate", json={"code": "code"}).status_code == 409


def test_keycloak_rotate_success_and_error_paths() -> None:
    deps = DummyDeps()
    deps.rotation_requested = False
    deps.completed_steps = {"profile_reviewed"}
    deps.admin = DummyAdmin(full={"requiredActions": ["CONFIGURE_TOTP"]})
    client = make_client(deps)

    response = client.post("/api/access/request/onboarding/keycloak-password-rotate", json={"code": "code"})
    assert response.status_code == 200
    assert deps.admin.updated == [("user-1", {"requiredActions": ["CONFIGURE_TOTP", "UPDATE_PASSWORD"]})]
    assert any("INSERT INTO access_request_onboarding_artifacts" in query for query, _ in deps.conn.executed)

    deps.rotation_requested = True
    deps.admin = DummyAdmin(full={"requiredActions": []})
    response = client.post("/api/access/request/onboarding/keycloak-password-rotate", json={"code": "code"})
    assert response.status_code == 200
    assert deps.admin.updated == []

    deps.admin = DummyAdmin(user={})
    assert client.post("/api/access/request/onboarding/keycloak-password-rotate", json={"code": "code"}).status_code == 409

    deps.conn = DummyConn(deps.request_row(), fail=True)
    assert client.post("/api/access/request/onboarding/keycloak-password-rotate", json={"code": "code"}).status_code == 502
