//! Shared test infrastructure for domain-layer unit tests.
//!
//! GTS registry: use `MockTypesRegistryClient` and `make_test_instance` from
//! `types_registry_sdk::testing` directly (gated on the `test-util`
//! dev-dependency feature).
//!
//! PDP: this module exposes `AuthZResolverClient` fakes plus `PolicyEnforcer`
//! constructors that let tests pin every outcome (permit + constraints, deny,
//! empty-constraints fail-closed, transport unreachable, and no-cache via the
//! per-call counting resolvers).
//!
//! Every public helper in this module is test-only — `.lock().expect("…")`
//! on the internal mutexes is fine because a poisoned mutex inside a unit
//! test indicates an unrecoverable test failure anyway. The
//! `missing_panics_doc` lint would force boilerplate `# Panics` sections
//! on every setter/getter; suppressing it module-wide keeps the helpers
//! readable without diluting the production-code expectation.
#![allow(clippy::missing_panics_doc)]

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use async_trait::async_trait;
use authz_resolver_sdk::constraints::Constraint;
use authz_resolver_sdk::models::{
    Capability, EvaluationRequest, EvaluationResponse, EvaluationResponseContext,
};
use authz_resolver_sdk::{AuthZResolverClient, AuthZResolverError, PolicyEnforcer};
use toolkit_odata::{ODataQuery, Page as ODataPage};
use usage_collector_sdk::{
    AggregationResult, AggregationSpec, MetadataFilter, UsageCollectorPluginError,
    UsageCollectorPluginV1, UsageRecord, UsageType, UsageTypeGtsId,
};
use uuid::Uuid;

/// Minimal mock storage-plugin client.
///
/// The `UsageCollectorPluginV1` SPI surface carries nine methods. The mock
/// here exists purely so the Plugin Host can resolve a concrete
/// `Arc<dyn UsageCollectorPluginV1>` from `ClientHub` and so cache tests can
/// assert `Arc::ptr_eq` on the resolved handle. Every method returns a
/// deterministic `Internal("test_fake: …")` so any test that accidentally
/// dispatches through the mock surfaces an obvious failure; downstream
/// features reshape the mock surface when test bodies need richer fakes.
pub struct MockPlugin;

impl MockPlugin {
    /// Returns a fresh mock plugin as a scoped trait object.
    #[must_use]
    pub fn arc() -> Arc<dyn UsageCollectorPluginV1> {
        Arc::new(Self)
    }
}

#[async_trait]
impl UsageCollectorPluginV1 for MockPlugin {
    async fn create_usage_record(
        &self,
        _record: UsageRecord,
    ) -> Result<UsageRecord, UsageCollectorPluginError> {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::create_usage_record not implemented",
        ))
    }

    async fn create_usage_records(
        &self,
        _records: Vec<UsageRecord>,
    ) -> Result<Vec<Result<UsageRecord, UsageCollectorPluginError>>, UsageCollectorPluginError>
    {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::create_usage_records not implemented",
        ))
    }

    async fn query_aggregated_usage_records(
        &self,
        _gts_id: UsageTypeGtsId,
        _query: &ODataQuery,
        _metadata_filter: &[MetadataFilter],
        _aggregation: AggregationSpec,
    ) -> Result<AggregationResult, UsageCollectorPluginError> {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::query_aggregated_usage_records not implemented",
        ))
    }

    async fn list_usage_records(
        &self,
        _gts_id: UsageTypeGtsId,
        _query: &ODataQuery,
        _metadata_filter: &[MetadataFilter],
    ) -> Result<ODataPage<UsageRecord>, UsageCollectorPluginError> {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::list_usage_records not implemented",
        ))
    }

    async fn deactivate_usage_record(&self, _id: Uuid) -> Result<(), UsageCollectorPluginError> {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::deactivate_usage_record not implemented",
        ))
    }

    async fn create_usage_type(
        &self,
        _usage_type: UsageType,
    ) -> Result<UsageType, UsageCollectorPluginError> {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::create_usage_type not implemented",
        ))
    }

    async fn get_usage_type(
        &self,
        _gts_id: UsageTypeGtsId,
    ) -> Result<UsageType, UsageCollectorPluginError> {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::get_usage_type not implemented",
        ))
    }

    async fn list_usage_types(
        &self,
        _query: &ODataQuery,
    ) -> Result<ODataPage<UsageType>, UsageCollectorPluginError> {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::list_usage_types not implemented",
        ))
    }

    async fn delete_usage_type(
        &self,
        _gts_id: UsageTypeGtsId,
    ) -> Result<(), UsageCollectorPluginError> {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::delete_usage_type not implemented",
        ))
    }

    async fn get_usage_record(
        &self,
        _uuid: Uuid,
    ) -> Result<UsageRecord, UsageCollectorPluginError> {
        Err(UsageCollectorPluginError::internal(
            "test_fake: MockPlugin::get_usage_record not implemented",
        ))
    }
}

// ── PDP (authz-resolver) mocks ─────────────────────────────────────────────

/// Build a permit `EvaluationResponse` carrying a single
/// `property = value` string-equality constraint. Compiles (against a resource
/// type that lists `property` as supported) to a non-empty `AccessScope`, so it
/// exercises the permit-with-constraints `Ok` path through
/// `require_constraints(true)`.
#[must_use]
pub fn permit_with_string_constraint(property: &'static str, value: String) -> EvaluationResponse {
    use authz_resolver_sdk::constraints::{EqPredicate, Predicate};

    EvaluationResponse {
        decision: true,
        context: EvaluationResponseContext {
            constraints: vec![Constraint {
                predicates: vec![Predicate::Eq(EqPredicate::new(property, value))],
            }],
            deny_reason: None,
        },
    }
}

/// Counting PDP fake that always permits with a fixed string-equality
/// constraint and records how many times it was called, so the no-cache test
/// can assert that two identical authorize calls each hit the resolver.
#[derive(Debug)]
pub struct CountingPermitResolver {
    property: &'static str,
    value: String,
    calls: AtomicUsize,
}

impl CountingPermitResolver {
    /// Build a resolver that permits with a single `property = value` string
    /// constraint.
    #[must_use]
    pub fn new(property: &'static str, value: String) -> Arc<Self> {
        Arc::new(Self {
            property,
            value,
            calls: AtomicUsize::new(0),
        })
    }

    /// Number of `evaluate` calls observed so far.
    #[must_use]
    pub fn calls(&self) -> usize {
        self.calls.load(Ordering::SeqCst)
    }
}

#[async_trait]
impl AuthZResolverClient for CountingPermitResolver {
    async fn evaluate(
        &self,
        _request: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        Ok(permit_with_string_constraint(
            self.property,
            self.value.clone(),
        ))
    }
}

/// Counting PDP fake that always permits with NO constraints (an
/// `allow_all` decision) and records call counts. Use this whenever the
/// resource type under test declares no supported PEP attributes — a
/// constraint-bearing permit (e.g. [`CountingPermitResolver`]) would fail
/// to compile under such a resource type, surfacing as
/// `EnforcerError::CompileFailed`.
#[derive(Debug, Default)]
pub struct CountingAllowAllResolver {
    calls: AtomicUsize,
}

impl CountingAllowAllResolver {
    /// Build an `allow_all` permit resolver.
    #[must_use]
    pub fn new() -> Arc<Self> {
        Arc::new(Self::default())
    }

    /// Number of `evaluate` calls observed so far.
    #[must_use]
    pub fn calls(&self) -> usize {
        self.calls.load(Ordering::SeqCst)
    }
}

#[async_trait]
impl AuthZResolverClient for CountingAllowAllResolver {
    async fn evaluate(
        &self,
        _request: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        Ok(EvaluationResponse {
            decision: true,
            context: EvaluationResponseContext {
                constraints: Vec::new(),
                deny_reason: None,
            },
        })
    }
}

/// PDP fake that permits with NO constraints AND captures the most-recent
/// [`EvaluationRequest`] it received, so tests can inspect the request shape
/// the PEP composed (rather than only its decision). Used by the
/// `authz_tests` equivalence regression to prove that the per-record and
/// per-tuple PDP composers emit byte-identical requests for the same input.
#[derive(Debug, Default)]
pub struct CapturingAllowAllResolver {
    last_request: std::sync::Mutex<Option<EvaluationRequest>>,
}

impl CapturingAllowAllResolver {
    /// Build a capturing allow-all resolver.
    #[must_use]
    pub fn new() -> Arc<Self> {
        Arc::new(Self::default())
    }

    /// Returns and clears the most-recently captured request.
    #[must_use]
    pub fn take_last_request(&self) -> Option<EvaluationRequest> {
        self.last_request.lock().expect("mutex").take()
    }
}

#[async_trait]
impl AuthZResolverClient for CapturingAllowAllResolver {
    async fn evaluate(
        &self,
        request: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        *self.last_request.lock().expect("mutex") = Some(request);
        Ok(EvaluationResponse {
            decision: true,
            context: EvaluationResponseContext {
                constraints: Vec::new(),
                deny_reason: None,
            },
        })
    }
}

/// PDP fake that permits but returns an EMPTY constraint set. With
/// `require_constraints(true)` the PEP fails this closed as
/// `EnforcerError::CompileFailed` (`empty_constraints`).
#[derive(Debug, Default)]
pub struct PermitEmptyConstraintsResolver;

#[async_trait]
impl AuthZResolverClient for PermitEmptyConstraintsResolver {
    async fn evaluate(
        &self,
        _request: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        Ok(EvaluationResponse {
            decision: true,
            context: EvaluationResponseContext {
                constraints: Vec::new(),
                deny_reason: None,
            },
        })
    }
}

/// PDP fake that denies every evaluation (`decision: false`), surfacing as
/// `EnforcerError::Denied` (`deny`).
#[derive(Debug, Default)]
pub struct DenyAllResolver;

#[async_trait]
impl AuthZResolverClient for DenyAllResolver {
    async fn evaluate(
        &self,
        _request: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        Ok(EvaluationResponse {
            decision: false,
            context: EvaluationResponseContext {
                constraints: Vec::new(),
                deny_reason: None,
            },
        })
    }
}

/// PDP fake whose transport is unreachable: every evaluation returns
/// `AuthZResolverError::ServiceUnavailable`, surfacing as
/// `EnforcerError::EvaluationFailed` (`unreachable`).
#[derive(Debug, Default)]
pub struct UnreachableResolver;

#[async_trait]
impl AuthZResolverClient for UnreachableResolver {
    async fn evaluate(
        &self,
        _request: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        Err(AuthZResolverError::ServiceUnavailable(
            "usage-collector test fake: simulated authz-resolver transport failure".to_owned(),
        ))
    }
}

/// PDP fake that combines an unreachable transport with a call counter,
/// so handler tests asserting a pre-service short-circuit can pin
/// `calls() == 0` as direct evidence the service path was never reached.
#[derive(Debug, Default)]
pub struct CountingUnreachableResolver {
    calls: AtomicUsize,
}

impl CountingUnreachableResolver {
    /// Build a counting unreachable resolver.
    #[must_use]
    pub fn new() -> Arc<Self> {
        Arc::new(Self::default())
    }

    /// Number of `evaluate` calls observed so far.
    #[must_use]
    pub fn calls(&self) -> usize {
        self.calls.load(Ordering::SeqCst)
    }
}

#[async_trait]
impl AuthZResolverClient for CountingUnreachableResolver {
    async fn evaluate(
        &self,
        _request: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        Err(AuthZResolverError::ServiceUnavailable(
            "usage-collector test fake: simulated authz-resolver transport failure".to_owned(),
        ))
    }
}

/// Wrap a `dyn AuthZResolverClient` in a `PolicyEnforcer` for tests, mirroring
/// the production `module.rs` wiring (`PolicyEnforcer::new(authz)`). The
/// `TenantHierarchy` capability is advertised so the request shape matches
/// production.
#[must_use]
pub fn enforcer_for(authz: Arc<dyn AuthZResolverClient>) -> PolicyEnforcer {
    PolicyEnforcer::new(authz).with_capabilities(vec![Capability::TenantHierarchy])
}

// ── Plugin Host wiring helpers (`ClientHub` + scoped plugin) ────────────────

use toolkit::client_hub::{ClientHub, ClientScope};
use toolkit_security::SecurityContext;
use types_registry_sdk::TypesRegistryClient;
use types_registry_sdk::testing::{MockTypesRegistryClient, make_test_instance};
use usage_collector_sdk::UsageCollectorPluginSpecV1;

use crate::domain::Service;

/// Build a usage-collector storage-plugin instance id under the schema
/// prefix advertised by [`UsageCollectorPluginSpecV1`], with `suffix` as
/// the five-token instance tail (e.g. `"test.happy_path.records.v1"`).
#[must_use]
pub fn usage_collector_instance_id(suffix: &str) -> String {
    format!("{}{suffix}", UsageCollectorPluginSpecV1::gts_type_id())
}

fn plugin_instance_content(gts_id: &str, vendor: &str) -> serde_json::Value {
    serde_json::json!({
        "id": gts_id,
        "vendor": vendor,
        "priority": 0,
        "properties": {}
    })
}

/// Wire a fresh [`ClientHub`] with a `MockTypesRegistryClient` advertising
/// one usage-collector plugin instance and a scoped client binding the
/// supplied `plugin` under that instance id.
#[must_use]
pub fn hub_with_plugin(
    plugin: Arc<dyn UsageCollectorPluginV1>,
    suffix: &str,
    vendor: &str,
) -> Arc<ClientHub> {
    let hub = Arc::new(ClientHub::default());
    let instance_id = usage_collector_instance_id(suffix);
    let instance = make_test_instance(&instance_id, plugin_instance_content(&instance_id, vendor));
    let registry: Arc<dyn TypesRegistryClient> =
        Arc::new(MockTypesRegistryClient::new().with_instances([instance]));
    hub.register::<dyn TypesRegistryClient>(registry);
    hub.register_scoped::<dyn UsageCollectorPluginV1>(ClientScope::gts_id(&instance_id), plugin);
    hub
}

/// Build a [`Service`] wired against a permit-by-default PDP and the
/// supplied plugin stub, registered under the `cyberfabric` vendor.
#[must_use]
pub fn service_with_permit(plugin: Arc<dyn UsageCollectorPluginV1>, suffix: &str) -> Arc<Service> {
    let hub = hub_with_plugin(plugin, suffix, "cyberfabric");
    let enforcer = enforcer_for(CountingAllowAllResolver::new());
    Arc::new(Service::new(hub, "cyberfabric".to_owned(), enforcer))
}

/// Variant of [`service_with_permit`] that exposes the underlying
/// [`CountingAllowAllResolver`] so tests can assert the exact number of
/// PDP `evaluate` round-trips the service issued.
#[must_use]
pub fn service_with_counting_permit(
    plugin: Arc<dyn UsageCollectorPluginV1>,
    suffix: &str,
) -> (Arc<Service>, Arc<CountingAllowAllResolver>) {
    let hub = hub_with_plugin(plugin, suffix, "cyberfabric");
    let resolver = CountingAllowAllResolver::new();
    let enforcer = enforcer_for(Arc::clone(&resolver) as Arc<dyn AuthZResolverClient>);
    let service = Arc::new(Service::new(hub, "cyberfabric".to_owned(), enforcer));
    (service, resolver)
}

/// Build an authenticated [`SecurityContext`] sufficient for PDP requests
/// composed from a [`UsageRecord`]'s attribution tuple.
#[must_use]
pub fn authenticated_ctx() -> SecurityContext {
    SecurityContext::builder()
        .subject_id(Uuid::from_u128(1))
        .subject_tenant_id(Uuid::from_u128(2))
        .subject_type("user")
        .build()
        .expect("authenticated context")
}

// ── HappyPathPlugin: programmable Ok-by-default SPI stub ────────────────────

use std::sync::Mutex;

/// Per-record outcome shape for a `create_usage_records` SPI batch
/// response — `Ok(persisted_record)` or
/// `Err(UsageCollectorPluginError)`. Factored out so the
/// `HappyPathPlugin` field type and the `set_create_records` parameter
/// type stay readable.
pub type CreateRecordsBatchResult = Vec<Result<UsageRecord, UsageCollectorPluginError>>;

/// Programmable plugin stub that returns the configured response for each
/// SPI method, defaulting to `UsageCollectorPluginError::internal("not
/// programmed")` for methods the test has not explicitly set up. Methods
/// also record their last-seen input so handler-level tests can verify
/// the service forwarded the expected argument.
///
/// The stub is `Arc<Self>` everywhere; interior state lives behind
/// `Mutex` so callers can program responses after construction.
pub struct HappyPathPlugin {
    create_record_response: Mutex<Option<Result<UsageRecord, UsageCollectorPluginError>>>,
    create_records_response: Mutex<Option<CreateRecordsBatchResult>>,
    deactivate_response: Mutex<Option<()>>,
    get_record_response: Mutex<Option<UsageRecord>>,
    create_usage_type_response: Mutex<Option<UsageType>>,
    get_usage_type_response: Mutex<Option<UsageType>>,
    /// `gts_id`s that should surface as
    /// `UsageCollectorPluginError::UsageTypeNotFound` instead of returning
    /// the default `get_usage_type_response`. Lets tests verify per-record
    /// not-found projection on the batch path.
    get_usage_type_not_found: Mutex<std::collections::BTreeSet<UsageTypeGtsId>>,
    list_usage_types_response: Mutex<Option<ODataPage<UsageType>>>,
    list_usage_records_response: Mutex<Option<ODataPage<UsageRecord>>>,
    query_aggregated_usage_records_response: Mutex<Option<AggregationResult>>,
    delete_usage_type_response: Mutex<Option<()>>,

    create_records_input: Mutex<Option<Vec<UsageRecord>>>,
    deactivate_input: Mutex<Option<Uuid>>,
    delete_usage_type_input: Mutex<Option<UsageTypeGtsId>>,
    create_usage_type_input: Mutex<Option<UsageType>>,
    /// Every `ODataQuery` ever forwarded to `list_usage_types`, in call
    /// order. Lets handler tests pin that the handler forwarded the
    /// query unchanged to the service (and hence to the SPI).
    list_usage_types_inputs: Mutex<Vec<ODataQuery>>,
    /// Every `gts_id` ever passed to `get_usage_type`, in call order.
    /// `len()` is the call count; the vec is forensic — tests can
    /// verify exactly which `gts_id`s the host looked up.
    get_usage_type_inputs: Mutex<Vec<UsageTypeGtsId>>,
    /// Every record `uuid` ever passed to `get_usage_record`, in call
    /// order. Drives the L1-corrects-id dedup tests the same way
    /// `get_usage_type_inputs` drives the catalog-dedup tests.
    get_usage_record_inputs: Mutex<Vec<Uuid>>,
    /// Record `uuid`s that should surface as
    /// `UsageCollectorPluginError::UsageRecordNotFound` instead of
    /// returning the default `get_record_response`.
    get_usage_record_not_found: Mutex<std::collections::BTreeSet<Uuid>>,
}

impl HappyPathPlugin {
    #[must_use]
    pub fn new() -> Arc<Self> {
        Arc::new(Self {
            create_record_response: Mutex::new(None),
            create_records_response: Mutex::new(None),
            deactivate_response: Mutex::new(None),
            get_record_response: Mutex::new(None),
            create_usage_type_response: Mutex::new(None),
            get_usage_type_response: Mutex::new(None),
            get_usage_type_not_found: Mutex::new(std::collections::BTreeSet::new()),
            list_usage_types_response: Mutex::new(None),
            list_usage_records_response: Mutex::new(None),
            query_aggregated_usage_records_response: Mutex::new(None),
            delete_usage_type_response: Mutex::new(None),
            create_records_input: Mutex::new(None),
            deactivate_input: Mutex::new(None),
            delete_usage_type_input: Mutex::new(None),
            create_usage_type_input: Mutex::new(None),
            list_usage_types_inputs: Mutex::new(Vec::new()),
            get_usage_type_inputs: Mutex::new(Vec::new()),
            get_usage_record_inputs: Mutex::new(Vec::new()),
            get_usage_record_not_found: Mutex::new(std::collections::BTreeSet::new()),
        })
    }

    pub fn set_create_record(&self, record: UsageRecord) {
        *self.create_record_response.lock().expect("mutex") = Some(Ok(record));
    }
    /// Program the singular `create_usage_record` SPI to return `err` on
    /// the next call. Used by tests that need to drive plugin-side
    /// failure modes (`Transient`, `IdempotencyConflict`, …) into the
    /// service's singular path.
    pub fn set_create_record_err(&self, err: UsageCollectorPluginError) {
        *self.create_record_response.lock().expect("mutex") = Some(Err(err));
    }
    pub fn set_create_records(&self, results: CreateRecordsBatchResult) {
        *self.create_records_response.lock().expect("mutex") = Some(results);
    }
    pub fn set_deactivate_ok(&self) {
        *self.deactivate_response.lock().expect("mutex") = Some(());
    }
    pub fn set_get_record(&self, record: UsageRecord) {
        *self.get_record_response.lock().expect("mutex") = Some(record);
    }
    pub fn set_create_usage_type(&self, ut: UsageType) {
        *self.create_usage_type_response.lock().expect("mutex") = Some(ut);
    }
    pub fn set_get_usage_type(&self, ut: UsageType) {
        *self.get_usage_type_response.lock().expect("mutex") = Some(ut);
    }
    /// Mark `gts_id` so the next (and every subsequent) `get_usage_type`
    /// call carrying it returns `UsageTypeNotFound` regardless of the
    /// default `get_usage_type_response`.
    pub fn set_get_usage_type_not_found(&self, gts_id: UsageTypeGtsId) {
        self.get_usage_type_not_found
            .lock()
            .expect("mutex")
            .insert(gts_id);
    }
    /// Every `gts_id` passed to `get_usage_type`, in call order.
    #[must_use]
    pub fn get_usage_type_inputs(&self) -> Vec<UsageTypeGtsId> {
        self.get_usage_type_inputs.lock().expect("mutex").clone()
    }
    /// Total number of `get_usage_type` SPI dispatches so far.
    #[must_use]
    pub fn get_usage_type_calls(&self) -> usize {
        self.get_usage_type_inputs.lock().expect("mutex").len()
    }
    /// Mark `uuid` so the next (and every subsequent) `get_usage_record`
    /// call carrying it returns `UsageRecordNotFound` regardless of the
    /// default `get_record_response`.
    pub fn set_get_usage_record_not_found(&self, uuid: Uuid) {
        self.get_usage_record_not_found
            .lock()
            .expect("mutex")
            .insert(uuid);
    }
    /// Every record `uuid` passed to `get_usage_record`, in call order.
    #[must_use]
    pub fn get_usage_record_inputs(&self) -> Vec<Uuid> {
        self.get_usage_record_inputs.lock().expect("mutex").clone()
    }
    /// Total number of `get_usage_record` SPI dispatches so far.
    #[must_use]
    pub fn get_usage_record_calls(&self) -> usize {
        self.get_usage_record_inputs.lock().expect("mutex").len()
    }
    pub fn set_list_usage_types(&self, page: ODataPage<UsageType>) {
        *self.list_usage_types_response.lock().expect("mutex") = Some(page);
    }
    pub fn set_list_usage_records_response(&self, page: ODataPage<UsageRecord>) {
        *self.list_usage_records_response.lock().expect("mutex") = Some(page);
    }
    pub fn set_query_aggregated_usage_records_response(&self, result: AggregationResult) {
        *self
            .query_aggregated_usage_records_response
            .lock()
            .expect("mutex") = Some(result);
    }
    pub fn set_delete_usage_type_ok(&self) {
        *self.delete_usage_type_response.lock().expect("mutex") = Some(());
    }

    pub fn last_create_records_input(&self) -> Option<Vec<UsageRecord>> {
        self.create_records_input.lock().expect("mutex").clone()
    }
    pub fn last_deactivate_input(&self) -> Option<Uuid> {
        *self.deactivate_input.lock().expect("mutex")
    }
    pub fn last_delete_usage_type_input(&self) -> Option<UsageTypeGtsId> {
        self.delete_usage_type_input.lock().expect("mutex").clone()
    }
    pub fn last_create_usage_type_input(&self) -> Option<UsageType> {
        self.create_usage_type_input.lock().expect("mutex").clone()
    }
    /// Every `ODataQuery` passed to `list_usage_types`, in call order.
    #[must_use]
    pub fn list_usage_types_inputs(&self) -> Vec<ODataQuery> {
        self.list_usage_types_inputs.lock().expect("mutex").clone()
    }
    /// The most-recent `ODataQuery` passed to `list_usage_types`, or
    /// `None` if the SPI was never invoked.
    #[must_use]
    pub fn last_list_usage_types_input(&self) -> Option<ODataQuery> {
        self.list_usage_types_inputs
            .lock()
            .expect("mutex")
            .last()
            .cloned()
    }
}

fn not_programmed(method: &'static str) -> UsageCollectorPluginError {
    UsageCollectorPluginError::internal(format!("HappyPathPlugin::{method} not programmed"))
}

#[async_trait]
impl UsageCollectorPluginV1 for HappyPathPlugin {
    async fn create_usage_record(
        &self,
        _record: UsageRecord,
    ) -> Result<UsageRecord, UsageCollectorPluginError> {
        // `take()` (not `clone()`) — `UsageCollectorPluginError` is
        // intentionally `!Clone` so tests program one outcome per call.
        match self.create_record_response.lock().expect("mutex").take() {
            Some(outcome) => outcome,
            None => Err(not_programmed("create_usage_record")),
        }
    }

    async fn create_usage_records(
        &self,
        records: Vec<UsageRecord>,
    ) -> Result<Vec<Result<UsageRecord, UsageCollectorPluginError>>, UsageCollectorPluginError>
    {
        *self.create_records_input.lock().expect("mutex") = Some(records);
        self.create_records_response
            .lock()
            .expect("mutex")
            .take()
            .ok_or_else(|| not_programmed("create_usage_records"))
    }

    async fn query_aggregated_usage_records(
        &self,
        _gts_id: UsageTypeGtsId,
        _query: &ODataQuery,
        _metadata_filter: &[MetadataFilter],
        _aggregation: AggregationSpec,
    ) -> Result<AggregationResult, UsageCollectorPluginError> {
        self.query_aggregated_usage_records_response
            .lock()
            .expect("mutex")
            .clone()
            .ok_or_else(|| not_programmed("query_aggregated_usage_records"))
    }

    async fn list_usage_records(
        &self,
        _gts_id: UsageTypeGtsId,
        _query: &ODataQuery,
        _metadata_filter: &[MetadataFilter],
    ) -> Result<ODataPage<UsageRecord>, UsageCollectorPluginError> {
        self.list_usage_records_response
            .lock()
            .expect("mutex")
            .clone()
            .ok_or_else(|| not_programmed("list_usage_records"))
    }

    async fn deactivate_usage_record(&self, id: Uuid) -> Result<(), UsageCollectorPluginError> {
        *self.deactivate_input.lock().expect("mutex") = Some(id);
        self.deactivate_response
            .lock()
            .expect("mutex")
            .ok_or_else(|| not_programmed("deactivate_usage_record"))
    }

    async fn create_usage_type(
        &self,
        usage_type: UsageType,
    ) -> Result<UsageType, UsageCollectorPluginError> {
        *self.create_usage_type_input.lock().expect("mutex") = Some(usage_type);
        self.create_usage_type_response
            .lock()
            .expect("mutex")
            .clone()
            .ok_or_else(|| not_programmed("create_usage_type"))
    }

    async fn get_usage_type(
        &self,
        gts_id: UsageTypeGtsId,
    ) -> Result<UsageType, UsageCollectorPluginError> {
        self.get_usage_type_inputs
            .lock()
            .expect("mutex")
            .push(gts_id.clone());
        if self
            .get_usage_type_not_found
            .lock()
            .expect("mutex")
            .contains(&gts_id)
        {
            return Err(UsageCollectorPluginError::UsageTypeNotFound { gts_id });
        }
        self.get_usage_type_response
            .lock()
            .expect("mutex")
            .clone()
            .ok_or_else(|| not_programmed("get_usage_type"))
    }

    async fn list_usage_types(
        &self,
        query: &ODataQuery,
    ) -> Result<ODataPage<UsageType>, UsageCollectorPluginError> {
        self.list_usage_types_inputs
            .lock()
            .expect("mutex")
            .push(query.clone());
        self.list_usage_types_response
            .lock()
            .expect("mutex")
            .clone()
            .ok_or_else(|| not_programmed("list_usage_types"))
    }

    async fn delete_usage_type(
        &self,
        gts_id: UsageTypeGtsId,
    ) -> Result<(), UsageCollectorPluginError> {
        *self.delete_usage_type_input.lock().expect("mutex") = Some(gts_id);
        self.delete_usage_type_response
            .lock()
            .expect("mutex")
            .ok_or_else(|| not_programmed("delete_usage_type"))
    }

    async fn get_usage_record(&self, uuid: Uuid) -> Result<UsageRecord, UsageCollectorPluginError> {
        self.get_usage_record_inputs
            .lock()
            .expect("mutex")
            .push(uuid);
        if self
            .get_usage_record_not_found
            .lock()
            .expect("mutex")
            .contains(&uuid)
        {
            return Err(UsageCollectorPluginError::UsageRecordNotFound { id: uuid });
        }
        self.get_record_response
            .lock()
            .expect("mutex")
            .clone()
            .ok_or_else(|| not_programmed("get_usage_record"))
    }
}
