use std::{collections::HashMap, sync::Arc};

use axum::{
    body::Body,
    http::{Request, StatusCode},
};
use philharmonic_api::{ApiStore, RequestScope};
use philharmonic_policy::{
    ALL_ATOMS, Principal, PrincipalKind, RoleDefinition, RoleMembership, Sck, Tenant, TenantStatus,
    TokenHash, atom, audit_event_type, generate_api_token,
};
use philharmonic_store::{ContentStore, EntityRefValue, RevisionRow};
use philharmonic_types::{
    CanonicalJson, ContentValue, EntityId, JsonValue, ScalarValue, Sha256, UnixMillis, Uuid,
};
use serde_json::json;
use tower::ServiceExt;

mod common;

const SCK_BYTES: [u8; 32] = [0x42; 32];
const KEY_VERSION: i64 = 7;

struct Fixture {
    router: axum::Router,
    principal: EntityId<Principal>,
    token: String,
}

async fn tenant_fixture(permissions: &[&str]) -> Fixture {
    tenant_fixture_with_store(common::MockStore::new(), permissions).await
}

async fn tenant_fixture_with_store(store: Arc<common::MockStore>, permissions: &[&str]) -> Fixture {
    let tenant = seed_tenant(&store, "Audit Producer Tenant", TenantStatus::Active).await;
    let (principal, token) = seed_principal_with_roles(&store, tenant, permissions).await;
    let router = router_for_scope(
        store.clone(),
        RequestScope::Tenant(tenant),
        Some(Sck::from_bytes(SCK_BYTES)),
    );
    Fixture {
        router,
        principal,
        token,
    }
}

fn router_for_scope(
    store: Arc<dyn ApiStore>,
    scope: RequestScope,
    sck: Option<Sck>,
) -> axum::Router {
    let mut builder = common::builder(
        Arc::new(common::FixedResolver::new(scope)),
        store,
        common::test_api_verifying_key_registry(),
    )
    .key_version(KEY_VERSION);
    if let Some(sck) = sck {
        builder = builder.sck(sck);
    }
    builder.build().unwrap().into_router()
}

async fn seed_tenant(
    store: &common::MockStore,
    display_name: &str,
    status: TenantStatus,
) -> EntityId<Tenant> {
    let tenant = common::new_typed_id::<Tenant>();
    let display_name = put_json(store, &JsonValue::String(display_name.to_string())).await;
    store.insert_entity(tenant);
    store.insert_revision(RevisionRow {
        entity_id: tenant.internal().as_uuid(),
        revision_seq: 0,
        created_at: UnixMillis(1),
        content_attrs: HashMap::from([("display_name".to_string(), display_name)]),
        entity_attrs: HashMap::new(),
        scalar_attrs: HashMap::from([("status".to_string(), ScalarValue::I64(status.as_i64()))]),
    });
    tenant
}

async fn seed_principal_with_roles(
    store: &common::MockStore,
    tenant: EntityId<Tenant>,
    permissions: &[&str],
) -> (EntityId<Principal>, String) {
    let (token, token_hash) = generate_api_token();
    let principal = seed_principal(
        store,
        tenant,
        "Audit Producer Principal",
        Some(token_hash_content_hash(token_hash)),
    )
    .await;
    let role = seed_role(store, tenant, "Audit Producer Role", permissions).await;
    seed_membership(store, tenant, principal, role).await;
    (principal, token.to_string())
}

async fn seed_principal(
    store: &common::MockStore,
    tenant: EntityId<Tenant>,
    display_name: &str,
    credential_hash: Option<Sha256>,
) -> EntityId<Principal> {
    let principal = common::new_typed_id::<Principal>();
    let display_name = put_json(store, &JsonValue::String(display_name.to_string())).await;
    let mut revision = RevisionRow {
        entity_id: principal.internal().as_uuid(),
        revision_seq: 0,
        created_at: UnixMillis(2),
        content_attrs: HashMap::from([("display_name".to_string(), display_name)]),
        entity_attrs: HashMap::from([(
            "tenant".to_string(),
            EntityRefValue::pinned(tenant.internal().as_uuid(), 0),
        )]),
        scalar_attrs: HashMap::from([
            (
                "kind".to_string(),
                ScalarValue::I64(PrincipalKind::User.as_i64()),
            ),
            ("epoch".to_string(), ScalarValue::I64(0)),
            ("is_retired".to_string(), ScalarValue::Bool(false)),
        ]),
    };
    if let Some(credential_hash) = credential_hash {
        revision
            .content_attrs
            .insert("credential_hash".to_string(), credential_hash);
    }
    store.insert_entity(principal);
    store.insert_revision(revision);
    principal
}

async fn seed_role(
    store: &common::MockStore,
    tenant: EntityId<Tenant>,
    display_name: &str,
    permissions: &[&str],
) -> EntityId<RoleDefinition> {
    let role = common::new_typed_id::<RoleDefinition>();
    let display_name = put_json(store, &JsonValue::String(display_name.to_string())).await;
    let permissions = put_json(store, &serde_json::to_value(permissions).unwrap()).await;
    store.insert_entity(role);
    store.insert_revision(RevisionRow {
        entity_id: role.internal().as_uuid(),
        revision_seq: 0,
        created_at: UnixMillis(3),
        content_attrs: HashMap::from([
            ("display_name".to_string(), display_name),
            ("permissions".to_string(), permissions),
        ]),
        entity_attrs: HashMap::from([(
            "tenant".to_string(),
            EntityRefValue::pinned(tenant.internal().as_uuid(), 0),
        )]),
        scalar_attrs: HashMap::from([("is_retired".to_string(), ScalarValue::Bool(false))]),
    });
    role
}

async fn seed_membership(
    store: &common::MockStore,
    tenant: EntityId<Tenant>,
    principal: EntityId<Principal>,
    role: EntityId<RoleDefinition>,
) -> EntityId<RoleMembership> {
    let membership = common::new_typed_id::<RoleMembership>();
    store.insert_entity(membership);
    store.insert_revision(RevisionRow {
        entity_id: membership.internal().as_uuid(),
        revision_seq: 0,
        created_at: UnixMillis(4),
        content_attrs: HashMap::new(),
        entity_attrs: HashMap::from([
            (
                "tenant".to_string(),
                EntityRefValue::pinned(tenant.internal().as_uuid(), 0),
            ),
            (
                "principal".to_string(),
                EntityRefValue::pinned(principal.internal().as_uuid(), 0),
            ),
            (
                "role".to_string(),
                EntityRefValue::pinned(role.internal().as_uuid(), 0),
            ),
        ]),
        scalar_attrs: HashMap::from([("is_retired".to_string(), ScalarValue::Bool(false))]),
    });
    membership
}

async fn put_json(store: &common::MockStore, value: &JsonValue) -> Sha256 {
    let canonical = CanonicalJson::from_value(value).unwrap();
    put_content(store, canonical.as_bytes()).await
}

async fn put_content(store: &common::MockStore, value: &[u8]) -> Sha256 {
    let content = ContentValue::new(value.to_vec());
    let hash = content.digest();
    store.put(&content).await.unwrap();
    hash
}

fn token_hash_content_hash(token_hash: TokenHash) -> Sha256 {
    ContentValue::new(token_hash.0.to_vec()).digest()
}

async fn api_json(
    router: &axum::Router,
    method: &str,
    uri: &str,
    token: &str,
    body: Option<JsonValue>,
    expected: StatusCode,
) -> JsonValue {
    let mut builder = Request::builder()
        .method(method)
        .uri(uri)
        .header("Authorization", format!("Bearer {token}"));
    if body.is_some() {
        builder = builder.header("Content-Type", "application/json");
    }
    let body = body
        .map(|value| Body::from(serde_json::to_vec(&value).unwrap()))
        .unwrap_or_else(Body::empty);
    let response = router
        .clone()
        .oneshot(builder.body(body).unwrap())
        .await
        .unwrap();
    let status = response.status();
    let body = axum::body::to_bytes(response.into_body(), usize::MAX)
        .await
        .unwrap();
    assert_eq!(
        status,
        expected,
        "response body: {}",
        String::from_utf8_lossy(&body)
    );
    serde_json::from_slice(&body).unwrap()
}

async fn audit_items(router: &axum::Router, token: &str) -> Vec<JsonValue> {
    api_json(router, "GET", "/v1/audit", token, None, StatusCode::OK).await["items"]
        .as_array()
        .unwrap()
        .clone()
}

fn assert_audit_event(
    items: &[JsonValue],
    event_type: i64,
    principal_id: Uuid,
    target_entity_id: Uuid,
    earliest: UnixMillis,
) -> JsonValue {
    let item = items
        .iter()
        .find(|item| {
            item["event_type"] == event_type
                && item["event_data"]["principal_id"] == principal_id.to_string()
                && item["event_data"]["target_entity_id"] == target_entity_id.to_string()
        })
        .unwrap_or_else(|| panic!("missing audit event type {event_type} for {target_entity_id}"));
    assert!(
        item["event_data"]["correlation_id"]
            .as_str()
            .is_some_and(|value| !value.is_empty())
    );
    let timestamp = item["timestamp"].as_i64().unwrap();
    assert!(timestamp >= earliest.as_i64());
    assert!(timestamp <= UnixMillis::now().as_i64() + 1_000);
    item.clone()
}

fn uuid_field(value: &JsonValue, field: &str) -> Uuid {
    value[field].as_str().unwrap().parse().unwrap()
}

#[tokio::test]
async fn principal_routes_emit_audit_events() {
    let fixture = tenant_fixture(&ALL_ATOMS).await;
    let actor = fixture.principal.public().as_uuid();
    let earliest = UnixMillis::now();
    let created = api_json(
        &fixture.router,
        "POST",
        "/v1/principals",
        &fixture.token,
        Some(json!({ "display_name": "Worker", "kind": "service" })),
        StatusCode::CREATED,
    )
    .await;
    let principal_id = uuid_field(&created, "principal_id");
    api_json(
        &fixture.router,
        "POST",
        &format!("/v1/principals/{principal_id}/rotate"),
        &fixture.token,
        None,
        StatusCode::OK,
    )
    .await;
    api_json(
        &fixture.router,
        "POST",
        &format!("/v1/principals/{principal_id}/retire"),
        &fixture.token,
        None,
        StatusCode::OK,
    )
    .await;

    let items = audit_items(&fixture.router, &fixture.token).await;
    assert_audit_event(
        &items,
        audit_event_type::PRINCIPAL_CREATED,
        actor,
        principal_id,
        earliest,
    );
    assert_audit_event(
        &items,
        audit_event_type::PRINCIPAL_CREDENTIAL_ROTATED,
        actor,
        principal_id,
        earliest,
    );
    assert_audit_event(
        &items,
        audit_event_type::PRINCIPAL_RETIRED,
        actor,
        principal_id,
        earliest,
    );
}

#[tokio::test]
async fn role_routes_emit_audit_events() {
    let fixture = tenant_fixture(&ALL_ATOMS).await;
    let actor = fixture.principal.public().as_uuid();
    let earliest = UnixMillis::now();
    let created = api_json(
        &fixture.router,
        "POST",
        "/v1/roles",
        &fixture.token,
        Some(json!({ "display_name": "Operator", "permissions": ["audit:read"] })),
        StatusCode::CREATED,
    )
    .await;
    let role_id = uuid_field(&created, "role_id");
    api_json(
        &fixture.router,
        "PATCH",
        &format!("/v1/roles/{role_id}"),
        &fixture.token,
        Some(json!({ "display_name": "Operator Updated" })),
        StatusCode::OK,
    )
    .await;
    api_json(
        &fixture.router,
        "POST",
        &format!("/v1/roles/{role_id}/retire"),
        &fixture.token,
        None,
        StatusCode::OK,
    )
    .await;

    let items = audit_items(&fixture.router, &fixture.token).await;
    assert_audit_event(
        &items,
        audit_event_type::ROLE_CREATED,
        actor,
        role_id,
        earliest,
    );
    assert_audit_event(
        &items,
        audit_event_type::ROLE_MODIFIED,
        actor,
        role_id,
        earliest,
    );
    assert_audit_event(
        &items,
        audit_event_type::ROLE_RETIRED,
        actor,
        role_id,
        earliest,
    );
}

#[tokio::test]
async fn membership_routes_emit_audit_events() {
    let fixture = tenant_fixture(&ALL_ATOMS).await;
    let actor = fixture.principal.public().as_uuid();
    let subject = api_json(
        &fixture.router,
        "POST",
        "/v1/principals",
        &fixture.token,
        Some(json!({ "display_name": "Subject", "kind": "service" })),
        StatusCode::CREATED,
    )
    .await;
    let subject_id = uuid_field(&subject, "principal_id");
    let role = api_json(
        &fixture.router,
        "POST",
        "/v1/roles",
        &fixture.token,
        Some(json!({ "display_name": "Subject Role", "permissions": ["audit:read"] })),
        StatusCode::CREATED,
    )
    .await;
    let role_id = uuid_field(&role, "role_id");
    let earliest = UnixMillis::now();
    let created = api_json(
        &fixture.router,
        "POST",
        "/v1/role-memberships",
        &fixture.token,
        Some(json!({ "principal_id": subject_id, "role_id": role_id })),
        StatusCode::CREATED,
    )
    .await;
    let membership_id = uuid_field(&created, "membership_id");
    api_json(
        &fixture.router,
        "DELETE",
        &format!("/v1/role-memberships/{membership_id}"),
        &fixture.token,
        None,
        StatusCode::OK,
    )
    .await;

    let items = audit_items(&fixture.router, &fixture.token).await;
    let created = assert_audit_event(
        &items,
        audit_event_type::ROLE_MEMBERSHIP_CREATED,
        actor,
        membership_id,
        earliest,
    );
    let removed = assert_audit_event(
        &items,
        audit_event_type::ROLE_MEMBERSHIP_REMOVED,
        actor,
        membership_id,
        earliest,
    );
    assert_eq!(
        created["event_data"]["subject_principal_id"],
        subject_id.to_string()
    );
    assert_eq!(created["event_data"]["role_id"], role_id.to_string());
    assert_eq!(
        removed["event_data"]["subject_principal_id"],
        subject_id.to_string()
    );
    assert_eq!(removed["event_data"]["role_id"], role_id.to_string());
}

#[tokio::test]
async fn endpoint_routes_emit_audit_events() {
    let fixture = tenant_fixture(&ALL_ATOMS).await;
    let actor = fixture.principal.public().as_uuid();
    let earliest = UnixMillis::now();
    let created = api_json(
        &fixture.router,
        "POST",
        "/v1/endpoints",
        &fixture.token,
        Some(json!({
            "display_name": "Endpoint",
            "implementation": "test",
            "config": { "url": "https://example.invalid" }
        })),
        StatusCode::CREATED,
    )
    .await;
    let endpoint_id = uuid_field(&created, "endpoint_id");
    api_json(
        &fixture.router,
        "POST",
        &format!("/v1/endpoints/{endpoint_id}/rotate"),
        &fixture.token,
        Some(json!({ "display_name": "Endpoint Rotated", "config": { "url": "https://rotated.example.invalid" } })),
        StatusCode::OK,
    )
    .await;
    api_json(
        &fixture.router,
        "POST",
        &format!("/v1/endpoints/{endpoint_id}/retire"),
        &fixture.token,
        None,
        StatusCode::OK,
    )
    .await;

    let items = audit_items(&fixture.router, &fixture.token).await;
    assert_audit_event(
        &items,
        audit_event_type::ENDPOINT_CREATED,
        actor,
        endpoint_id,
        earliest,
    );
    assert_audit_event(
        &items,
        audit_event_type::ENDPOINT_ROTATED,
        actor,
        endpoint_id,
        earliest,
    );
    assert_audit_event(
        &items,
        audit_event_type::ENDPOINT_RETIRED,
        actor,
        endpoint_id,
        earliest,
    );
}

#[tokio::test]
async fn authority_routes_emit_audit_events() {
    let fixture = tenant_fixture(&ALL_ATOMS).await;
    let actor = fixture.principal.public().as_uuid();
    let earliest = UnixMillis::now();
    let created = api_json(
        &fixture.router,
        "POST",
        "/v1/minting-authorities",
        &fixture.token,
        Some(json!({
            "display_name": "Authority",
            "permission_envelope": [atom::MINT_EPHEMERAL_TOKEN],
            "max_lifetime_seconds": 600
        })),
        StatusCode::CREATED,
    )
    .await;
    let authority_id = uuid_field(&created, "authority_id");
    api_json(
        &fixture.router,
        "PATCH",
        &format!("/v1/minting-authorities/{authority_id}"),
        &fixture.token,
        Some(json!({ "display_name": "Authority Updated" })),
        StatusCode::OK,
    )
    .await;
    api_json(
        &fixture.router,
        "POST",
        &format!("/v1/minting-authorities/{authority_id}/rotate"),
        &fixture.token,
        None,
        StatusCode::OK,
    )
    .await;
    let bumped = api_json(
        &fixture.router,
        "POST",
        &format!("/v1/minting-authorities/{authority_id}/bump-epoch"),
        &fixture.token,
        None,
        StatusCode::OK,
    )
    .await;
    api_json(
        &fixture.router,
        "POST",
        &format!("/v1/minting-authorities/{authority_id}/retire"),
        &fixture.token,
        None,
        StatusCode::OK,
    )
    .await;

    let items = audit_items(&fixture.router, &fixture.token).await;
    assert_audit_event(
        &items,
        audit_event_type::AUTHORITY_CREATED,
        actor,
        authority_id,
        earliest,
    );
    assert_audit_event(
        &items,
        audit_event_type::AUTHORITY_MODIFIED,
        actor,
        authority_id,
        earliest,
    );
    let bumped_event = assert_audit_event(
        &items,
        audit_event_type::AUTHORITY_EPOCH_BUMPED,
        actor,
        authority_id,
        earliest,
    );
    assert_eq!(bumped_event["event_data"]["new_epoch"], bumped["epoch"]);
    assert_audit_event(
        &items,
        audit_event_type::AUTHORITY_RETIRED,
        actor,
        authority_id,
        earliest,
    );
}

#[tokio::test]
async fn mint_route_emits_privacy_restricted_audit_event() {
    let fixture = tenant_fixture(&ALL_ATOMS).await;
    let created = api_json(
        &fixture.router,
        "POST",
        "/v1/minting-authorities",
        &fixture.token,
        Some(json!({
            "display_name": "Minting Authority",
            "permission_envelope": [atom::MINT_EPHEMERAL_TOKEN, atom::AUDIT_READ],
            "max_lifetime_seconds": 600
        })),
        StatusCode::CREATED,
    )
    .await;
    let authority_id = uuid_field(&created, "authority_id");
    let authority_token = created["token"].as_str().unwrap();
    let earliest = UnixMillis::now();
    let minted = api_json(
        &fixture.router,
        "POST",
        "/v1/tokens/mint",
        authority_token,
        Some(json!({
            "subject": "end-user-123",
            "lifetime_seconds": 60,
            "instance_id": null,
            "requested_permissions": [atom::AUDIT_READ],
            "injected_claims": { "customer_id": "private-tenant-data" }
        })),
        StatusCode::OK,
    )
    .await;

    let items = audit_items(&fixture.router, &fixture.token).await;
    let event = assert_audit_event(
        &items,
        audit_event_type::TOKEN_MINTED,
        authority_id,
        authority_id,
        earliest,
    );
    assert_eq!(event["event_data"]["subject"]["subject_id"], "end-user-123");
    assert_eq!(
        event["event_data"]["subject"]["authority_id"],
        authority_id.to_string()
    );
    for key in [
        "claims",
        "permissions",
        "token",
        "token_hex",
        "lifetime_ms",
        "injected_claims",
        "requested_permissions",
        "expires_at",
    ] {
        assert!(event["event_data"].get(key).is_none(), "{key} leaked");
    }
    let token = minted["token"].as_str().unwrap();
    assert!(!event["event_data"].to_string().contains(token));
}

#[tokio::test]
async fn operator_tenant_status_routes_emit_target_tenant_audit_events() {
    let store = common::MockStore::new();
    let operator_tenant = seed_tenant(&store, "Operator Tenant", TenantStatus::Active).await;
    let (operator_principal, operator_token) =
        seed_principal_with_roles(&store, operator_tenant, &ALL_ATOMS).await;
    let target_tenant = seed_tenant(&store, "Target Tenant", TenantStatus::Active).await;
    let (_reader_principal, reader_token) =
        seed_principal_with_roles(&store, target_tenant, &ALL_ATOMS).await;
    let operator_router = router_for_scope(store.clone(), RequestScope::Operator, None);
    let tenant_router = router_for_scope(store, RequestScope::Tenant(target_tenant), None);
    let actor = operator_principal.public().as_uuid();
    let target = target_tenant.public().as_uuid();
    let earliest = UnixMillis::now();

    api_json(
        &operator_router,
        "POST",
        &format!("/v1/operator/tenants/{target}/suspend"),
        &operator_token,
        None,
        StatusCode::OK,
    )
    .await;
    api_json(
        &operator_router,
        "POST",
        &format!("/v1/operator/tenants/{target}/unsuspend"),
        &operator_token,
        None,
        StatusCode::OK,
    )
    .await;

    let items = audit_items(&tenant_router, &reader_token).await;
    let suspend = items
        .iter()
        .find(|item| {
            item["event_type"] == audit_event_type::TENANT_STATUS_CHANGED
                && item["event_data"]["principal_id"] == actor.to_string()
                && item["event_data"]["target_entity_id"] == target.to_string()
                && item["event_data"]["from_status"] == "active"
                && item["event_data"]["to_status"] == "suspended"
        })
        .expect("suspend audit event exists");
    let timestamp = suspend["timestamp"].as_i64().unwrap();
    assert!(timestamp >= earliest.as_i64());
    assert!(timestamp <= UnixMillis::now().as_i64() + 1_000);
    assert_eq!(suspend["event_data"]["from_status"], "active");
    assert_eq!(suspend["event_data"]["to_status"], "suspended");
    assert!(items.iter().any(|item| {
        item["event_type"] == audit_event_type::TENANT_STATUS_CHANGED
            && item["event_data"]["principal_id"] == actor.to_string()
            && item["event_data"]["target_entity_id"] == target.to_string()
            && item["event_data"]["from_status"] == "suspended"
            && item["event_data"]["to_status"] == "active"
    }));
}
