use self::mock::{
    assert_mock, assert_single_mock, assert_single_mock_count, mock, mock_additional,
};
use crate::{agent::Status, export::Principal, Agent, AgentError, Certificate};
use candid::{Encode, Nat};
use futures_util::FutureExt;
use ic_certification::{Delegation, Label};
use ic_transport_types::{
    NodeSignature, QueryResponse, RejectCode, RejectResponse, ReplyResponse, TransportCallResponse,
};
use std::{collections::BTreeMap, str::FromStr, sync::Arc, time::Duration};
#[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))]
use wasm_bindgen_test::wasm_bindgen_test;

use crate::agent::route_provider::{RoundRobinRouteProvider, RouteProvider};
#[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

fn make_agent(url: &str) -> Agent {
    let builder = Agent::builder().with_url(url);
    builder.with_verify_query_signatures(false).build().unwrap()
}

fn make_agent_with_route_provider(
    route_provider: Arc<dyn RouteProvider>,
    tcp_retries: usize,
) -> Agent {
    Agent::builder()
        .with_arc_route_provider(route_provider)
        .with_max_tcp_error_retries(tcp_retries)
        .with_verify_query_signatures(false)
        .build()
        .unwrap()
}

fn make_untimed_agent(url: &str) -> Agent {
    Agent::builder()
        .with_url(url)
        .with_verify_query_signatures(false)
        .with_ingress_expiry(Duration::from_secs(u32::MAX.into()))
        .build()
        .unwrap()
}

fn make_certifying_agent(url: &str) -> Agent {
    Agent::builder()
        .with_url(url)
        .with_ingress_expiry(Duration::from_secs(u32::MAX.into()))
        .build()
        .unwrap()
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn query() -> Result<(), AgentError> {
    let blob = Vec::from("Hello World");
    let response = QueryResponse::Replied {
        reply: ReplyResponse { arg: blob.clone() },
        signatures: vec![],
    };

    let (query_mock, url) = mock(
        "POST",
        "/api/v3/canister/aaaaa-aa/query",
        200,
        serde_cbor::to_vec(&response)?,
        Some("application/cbor"),
    )
    .await;

    let agent = make_agent(&url);
    let result = agent
        .query_raw(
            Principal::management_canister(),
            Principal::management_canister(),
            "main".to_string(),
            vec![],
            None,
            false,
            None,
        )
        .await;

    assert_mock(query_mock).await;

    assert_eq!(result?, blob);

    Ok(())
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn query_error() -> Result<(), AgentError> {
    let (query_mock, url) =
        mock("POST", "/api/v3/canister/aaaaa-aa/query", 500, vec![], None).await;
    let agent = make_agent(&url);

    let result = agent
        .query_raw(
            Principal::management_canister(),
            Principal::management_canister(),
            "greet".to_string(),
            vec![],
            None,
            false,
            None,
        )
        .await;

    assert_mock(query_mock).await;

    assert!(result.is_err());

    Ok(())
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn query_rejected() -> Result<(), AgentError> {
    let response: QueryResponse = QueryResponse::Rejected {
        reject: RejectResponse {
            reject_code: RejectCode::DestinationInvalid,
            reject_message: "Rejected Message".to_string(),
            error_code: Some("Error code".to_string()),
        },
        signatures: vec![],
    };

    let (query_mock, url) = mock(
        "POST",
        "/api/v3/canister/aaaaa-aa/query",
        200,
        serde_cbor::to_vec(&response)?,
        Some("application/cbor"),
    )
    .await;

    let agent = make_agent(&url);

    let result = agent
        .query_raw(
            Principal::management_canister(),
            Principal::management_canister(),
            "greet".to_string(),
            vec![],
            None,
            false,
            None,
        )
        .await;

    assert_mock(query_mock).await;

    match result {
        Err(AgentError::UncertifiedReject {
            reject: replica_error,
            ..
        }) => {
            assert_eq!(replica_error.reject_code, RejectCode::DestinationInvalid);
            assert_eq!(replica_error.reject_message, "Rejected Message");
            assert_eq!(replica_error.error_code, Some("Error code".to_string()));
        }
        result => unreachable!("{:?}", result),
    }

    Ok(())
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn call_error() -> Result<(), AgentError> {
    let (call_mock, url) = mock("POST", "/api/v4/canister/aaaaa-aa/call", 500, vec![], None).await;

    let agent = make_agent(&url);

    let result = agent
        .update(&Principal::management_canister(), "greet")
        .with_arg([])
        .call()
        .await;

    assert_mock(call_mock).await;

    assert!(result.is_err());

    Ok(())
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn call_rejected() -> Result<(), AgentError> {
    let reject_response = RejectResponse {
        reject_code: RejectCode::SysTransient,
        reject_message: "Test reject message".to_string(),
        error_code: Some("Test error code".to_string()),
    };

    let reject_body = TransportCallResponse::NonReplicatedRejection(reject_response.clone());

    let body = serde_cbor::to_vec(&reject_body).unwrap();

    let (call_mock, url) = mock(
        "POST",
        "/api/v4/canister/aaaaa-aa/call",
        200,
        body,
        Some("application/cbor"),
    )
    .await;

    let agent = make_agent(&url);

    let result = agent
        .update(&Principal::management_canister(), "greet")
        .with_arg([])
        .call()
        .await;

    assert_mock(call_mock).await;

    assert!(
        matches!(result, Err(AgentError::UncertifiedReject { reject, .. }) if reject == reject_response)
    );

    Ok(())
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn call_rejected_without_error_code() -> Result<(), AgentError> {
    let non_replicated_reject = RejectResponse {
        reject_code: RejectCode::SysTransient,
        reject_message: "Test reject message".to_string(),
        error_code: None,
    };

    let reject_body = TransportCallResponse::NonReplicatedRejection(non_replicated_reject.clone());

    let canister_id_str = "aaaaa-aa";

    let body = serde_cbor::to_vec(&reject_body).unwrap();

    let (call_mock, url) = mock(
        "POST",
        format!("/api/v4/canister/{canister_id_str}/call").as_str(),
        200,
        body,
        Some("application/cbor"),
    )
    .await;

    let agent = make_agent(&url);

    let result = agent
        .update(&Principal::from_str(canister_id_str).unwrap(), "greet")
        .with_arg([])
        .call()
        .await;

    assert_mock(call_mock).await;

    assert!(
        matches!(result, Err(AgentError::UncertifiedReject { reject, .. }) if reject == non_replicated_reject)
    );

    Ok(())
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn status() -> Result<(), AgentError> {
    let map = BTreeMap::new();
    let response = serde_cbor::Value::Map(map);
    let (read_mock, url) = mock(
        "GET",
        "/api/v2/status",
        200,
        serde_cbor::to_vec(&response)?,
        Some("application/cbor"),
    )
    .await;

    let agent = make_agent(&url);
    let result = agent.status().await;

    assert_mock(read_mock).await;
    assert!(matches!(result, Ok(Status { .. })));

    Ok(())
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn status_okay() -> Result<(), AgentError> {
    let map = BTreeMap::new();
    let response = serde_cbor::Value::Map(map);
    let (read_mock, url) = mock(
        "GET",
        "/api/v2/status",
        200,
        serde_cbor::to_vec(&response)?,
        Some("application/cbor"),
    )
    .await;

    let agent = make_agent(&url);
    let result = agent.status().await;

    assert_mock(read_mock).await;

    assert!(result.is_ok());

    Ok(())
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
async fn reqwest_client_status_okay_when_request_retried() -> Result<(), AgentError> {
    let map = BTreeMap::new();
    let response = serde_cbor::Value::Map(map);
    let (read_mock, url) = mock(
        "GET",
        "/api/v2/status",
        200,
        serde_cbor::to_vec(&response)?,
        Some("application/cbor"),
    )
    .await;
    // Without retry request should fail.
    let non_working_url = "http://127.0.0.1:4444";
    let tcp_retries = 0;
    let route_provider = RoundRobinRouteProvider::new(vec![non_working_url, &url]).unwrap();
    let agent = make_agent_with_route_provider(Arc::new(route_provider), tcp_retries);
    let result = agent.status().await;
    assert!(result.is_err());

    // With retry request should succeed.
    let tcp_retries = 1;
    let route_provider = RoundRobinRouteProvider::new(vec![non_working_url, &url]).unwrap();
    let agent = make_agent_with_route_provider(Arc::new(route_provider), tcp_retries);
    let result = agent.status().await;

    assert_mock(read_mock).await;

    assert!(result.is_ok());
    Ok(())
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
// test that the agent (re)tries to reach the server.
// We spawn an agent that waits 400ms between requests, and times out after 600ms. The agent is
// expected to hit the server at ~ 0ms and ~ 400 ms, and then shut down at 600ms, so we check that
// the server got two requests.
async fn status_error() -> Result<(), AgentError> {
    // This mock is never asserted as we don't know (nor do we need to know) how many times
    // it is called.
    let (_read_mock, url) = mock("GET", "/api/v2/status", 500, vec![], None).await;

    let agent = make_agent(&url);
    let result = agent.status().await;

    assert!(result.is_err());

    Ok(())
}

const REQ_WITH_DELEGATED_CERT_PATH: [&str; 1] = ["time"];
const REQ_WITH_DELEGATED_CERT_CANISTER: &str = "ivg37-qiaaa-aaaab-aaaga-cai";
// This file is a mainnet POST response body to /api/v3/canister/ivg37-.../read_state with path /time
const REQ_WITH_DELEGATED_CERT_RESPONSE: &[u8] = include_bytes!("agent_test/ivg37_time.bin");

// this is the same response as REQ_WITH_DELEGATED_CERT_RESPONSE, but with a manually pruned /canister_ranges.
// Run the ref-tests bin prune-ranges to generate it.
const PRUNED_RANGES: &[u8] = include_bytes!("agent_test/ivg37_time_pruned_ranges.bin");

// This file is a mainnet POST response body to /api/v2/canister/ivg37-.../read_state (old format).
// The delegation cert uses /subnet/<subnet_id>/canister_ranges instead of /canister_ranges/<subnet_id>/<shard>.
const DELEGATED_CERT_SUBNET_RANGES: &[u8] = include_bytes!("agent_test/ivg37_delegated_v2.bin");
// Same as above, but with /subnet/<subnet_id>/canister_ranges manually pruned.
const DELEGATED_CERT_SUBNET_RANGES_PRUNED: &[u8] =
    include_bytes!("agent_test/ivg37_delegated_v2_pruned_subnet.bin");

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
// asserts that a delegated certificate with correct /subnet/<subnetid>/canister_ranges
// passes the certificate verification
async fn check_subnet_range_with_valid_range() {
    let (_read_mock, url) = mock(
        "POST",
        "/api/v3/canister/ivg37-qiaaa-aaaab-aaaga-cai/read_state",
        200,
        REQ_WITH_DELEGATED_CERT_RESPONSE.into(),
        Some("application/cbor"),
    )
    .await;
    let agent = make_untimed_agent(&url);
    let _result = agent
        .read_state_raw(
            vec![REQ_WITH_DELEGATED_CERT_PATH
                .into_iter()
                .map(Label::from)
                .collect()],
            Principal::from_text(REQ_WITH_DELEGATED_CERT_CANISTER).unwrap(),
        )
        .await
        .expect("read state failed");
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
// asserts that a delegated certificate with /subnet/<subnetid>/canister_ranges that don't include
// the canister gets rejected by the cert verification because the subnet is not authorized to
// respond to requests for this canister. We do this by using a correct response but serving it
// for the wrong canister, which a malicious node might do.
async fn check_subnet_range_with_unauthorized_range() {
    let wrong_canister = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
    let (_read_mock, url) = mock(
        "POST",
        "/api/v3/canister/ryjl3-tyaaa-aaaaa-aaaba-cai/read_state",
        200,
        REQ_WITH_DELEGATED_CERT_RESPONSE.into(),
        Some("application/cbor"),
    )
    .await;
    let agent = make_untimed_agent(&url);
    let result = agent
        .read_state_raw(
            vec![REQ_WITH_DELEGATED_CERT_PATH
                .into_iter()
                .map(Label::from)
                .collect()],
            wrong_canister,
        )
        .await;
    assert_eq!(result, Err(AgentError::CertificateNotAuthorized()));
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
// asserts that a delegated certificate with pruned/removed /subnet/<subnetid>/canister_ranges
// gets rejected by the cert verification. We do this by using a correct response that has
// the leaf manually pruned
async fn check_subnet_range_with_pruned_range() {
    let canister = Principal::from_text("ivg37-qiaaa-aaaab-aaaga-cai").unwrap();
    let (_read_mock, url) = mock(
        "POST",
        "/api/v3/canister/ivg37-qiaaa-aaaab-aaaga-cai/read_state",
        200,
        PRUNED_RANGES.into(),
        Some("application/cbor"),
    )
    .await;
    let agent = make_untimed_agent(&url);
    let result = agent
        .read_state_raw(
            vec![REQ_WITH_DELEGATED_CERT_PATH
                .into_iter()
                .map(Label::from)
                .collect()],
            canister,
        )
        .await;
    assert!(result.is_err());
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
// asserts that a delegated certificate using the older /subnet/<subnetid>/canister_ranges path
// passes certificate verification via the fallback path in check_delegation
async fn check_subnet_range_fallback_valid_range() {
    let (_read_mock, url) = mock(
        "POST",
        "/api/v3/canister/ivg37-qiaaa-aaaab-aaaga-cai/read_state",
        200,
        DELEGATED_CERT_SUBNET_RANGES.into(),
        Some("application/cbor"),
    )
    .await;
    let agent = make_untimed_agent(&url);
    let _result = agent
        .read_state_raw(
            vec![REQ_WITH_DELEGATED_CERT_PATH
                .into_iter()
                .map(Label::from)
                .collect()],
            Principal::from_text(REQ_WITH_DELEGATED_CERT_CANISTER).unwrap(),
        )
        .await
        .expect("read state failed");
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
// asserts that a delegated certificate using /subnet/<subnetid>/canister_ranges that doesn't
// include the canister gets rejected via the fallback path
async fn check_subnet_range_fallback_unauthorized_range() {
    let wrong_canister = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
    let (_read_mock, url) = mock(
        "POST",
        "/api/v3/canister/ryjl3-tyaaa-aaaaa-aaaba-cai/read_state",
        200,
        DELEGATED_CERT_SUBNET_RANGES.into(),
        Some("application/cbor"),
    )
    .await;
    let agent = make_untimed_agent(&url);
    let result = agent
        .read_state_raw(
            vec![REQ_WITH_DELEGATED_CERT_PATH
                .into_iter()
                .map(Label::from)
                .collect()],
            wrong_canister,
        )
        .await;
    assert_eq!(result, Err(AgentError::CertificateNotAuthorized()));
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
// asserts that a delegated certificate with both canister range paths absent or pruned
// fails cert verification (exercises the fallback returning an error)
async fn check_subnet_range_fallback_pruned_range() {
    let canister = Principal::from_text("ivg37-qiaaa-aaaab-aaaga-cai").unwrap();
    let (_read_mock, url) = mock(
        "POST",
        "/api/v3/canister/ivg37-qiaaa-aaaab-aaaga-cai/read_state",
        200,
        DELEGATED_CERT_SUBNET_RANGES_PRUNED.into(),
        Some("application/cbor"),
    )
    .await;
    let agent = make_untimed_agent(&url);
    let result = agent
        .read_state_raw(
            vec![REQ_WITH_DELEGATED_CERT_PATH
                .into_iter()
                .map(Label::from)
                .collect()],
            canister,
        )
        .await;
    assert!(result.is_err());
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn wrong_subnet_query_certificate() {
    // these responses are for canister 224od-giaaa-aaaao-ae5vq-cai
    let wrong_canister = Principal::from_text("rdmx6-jaaaa-aaaaa-aaadq-cai").unwrap();
    let (mut read_mock, url) = mock(
        "POST",
        &format!("/api/v3/canister/{wrong_canister}/read_state"),
        200,
        SUBNET_224OD.into(),
        Some("application/cbor"),
    )
    .await;
    let blob = Encode!(&Nat::from(12u8)).unwrap();
    let response = QueryResponse::Replied {
        reply: ReplyResponse { arg: blob.clone() },
        signatures: vec![NodeSignature {
            timestamp: 1_697_831_349_698_624_964,
            signature: hex::decode("4bb6ba316623395d56d8e2834ece39d2c81d47e76a9fd122e1457963be6a83a5589e2c98c7b4d8b3c6c7b11c74b8ce9dcb345b5d1bd91706a643f33c7b509b0b").unwrap(),
            identity: "oo4np-rrvnz-5vram-kglex-enhkp-uew6q-vdf6z-whj4x-v44jd-tebaw-nqe".parse().unwrap()
        }],
    };
    mock_additional(
        &mut read_mock,
        "POST",
        &format!("/api/v3/canister/{wrong_canister}/query"),
        200,
        serde_cbor::to_vec(&response).unwrap(),
        Some("application/cbor"),
    )
    .await;
    let agent = make_certifying_agent(&url);
    let result = agent.query(&wrong_canister, "getVersion").call().await;
    assert!(matches!(
        dbg!(result.unwrap_err()),
        AgentError::CertificateNotAuthorized()
    ));
    assert_single_mock(
        "POST",
        &format!("/api/v3/canister/{wrong_canister}/read_state"),
        &read_mock,
    )
    .await;
}

// This file is a mainnet POST response body to /api/v3/canister/224od-.../read_state with path /subnet
const SUBNET_224OD: &[u8] = include_bytes!("agent_test/224od_subnet.bin");

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn no_cert() {
    let canister = Principal::from_text("224od-giaaa-aaaao-ae5vq-cai").unwrap();
    let (mut read_mock, url) = mock(
        "POST",
        "/api/v3/canister/224od-giaaa-aaaao-ae5vq-cai/read_state",
        200,
        SUBNET_224OD.into(),
        Some("application/cbor"),
    )
    .await;
    let blob = Encode!(&Nat::from(12u8)).unwrap();
    let response = QueryResponse::Replied {
        reply: ReplyResponse { arg: blob.clone() },
        signatures: vec![],
    };
    mock_additional(
        &mut read_mock,
        "POST",
        "/api/v3/canister/224od-giaaa-aaaao-ae5vq-cai/query",
        200,
        serde_cbor::to_vec(&response).unwrap(),
        Some("application/cbor"),
    )
    .await;
    let agent = make_certifying_agent(&url);
    let result = agent.query(&canister, "getVersion").call().await;
    assert!(matches!(result.unwrap_err(), AgentError::MissingSignature));
    assert_mock(read_mock).await;
}
// This file is a mainnet POST response body to /api/v3/subnet/uzr34-.../read_state with paths /canister_ranges/hex(uzr34-...) and /subnet/hex(uzr34-...)
const NODE_KEYS_UZR34: &[u8] = include_bytes!("agent_test/uzr34_node_keys.bin");

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn too_many_delegations() {
    // Use the certificate as its own delegation, and repeat the process the specified number of times
    fn self_delegate_cert(subnet_id: &[u8], cert: &Certificate, depth: u32) -> Certificate {
        let mut current = cert.clone();
        for _ in 0..depth {
            current = Certificate {
                tree: current.tree.clone(),
                signature: current.signature.clone(),
                delegation: Some(Delegation {
                    subnet_id: subnet_id.to_vec(),
                    certificate: serde_cbor::to_vec(&current).unwrap(),
                }),
            }
        }
        current
    }

    let subnet_id =
        Principal::from_text("uzr34-akd3s-xrdag-3ql62-ocgoh-ld2ao-tamcv-54e7j-krwgb-2gm4z-oqe")
            .unwrap();

    let (_read_mock, url) = mock(
        "POST",
        format!("/api/v3/subnet/{subnet_id}/read_state").as_str(),
        200,
        NODE_KEYS_UZR34.into(),
        Some("application/cbor"),
    )
    .await;
    let path_label = Label::from_bytes("subnet".as_bytes());
    let agent = make_untimed_agent(&url);
    let cert = agent
        .read_subnet_state_raw(vec![vec![path_label]], subnet_id)
        .await
        .expect("read state failed");
    let new_cert = self_delegate_cert(subnet_id.as_slice(), &cert, 1);
    assert!(matches!(
        agent.verify_for_subnet(&new_cert, subnet_id).unwrap_err(),
        AgentError::CertificateHasTooManyDelegations
    ));
}

#[cfg_attr(not(target_family = "wasm"), tokio::test)]
#[cfg_attr(target_family = "wasm", wasm_bindgen_test)]
async fn retry_ratelimit() {
    let (mut mock, url) = mock(
        "POST",
        "/api/v3/canister/ryjl3-tyaaa-aaaaa-aaaba-cai/query",
        429,
        vec![],
        Some("text/plain"),
    )
    .await;
    let agent = make_agent(&url);
    futures_util::select! {
        _ = agent.query(&"ryjl3-tyaaa-aaaaa-aaaba-cai".parse().unwrap(), "greet").call().fuse() => panic!("did not retry 429"),
        _ = crate::util::sleep(Duration::from_millis(500)).fuse() => {},
    };
    assert_single_mock_count(
        "POST",
        "/api/v3/canister/ryjl3-tyaaa-aaaaa-aaaba-cai/query",
        2,
        &mut mock,
    )
    .await;
}

#[cfg(not(target_family = "wasm"))]
mod mock {

    use std::collections::HashMap;

    use mockito::{Mock, Server, ServerGuard};

    pub async fn mock(
        method: &str,
        path: &str,
        status_code: u16,
        body: Vec<u8>,
        content_type: Option<&str>,
    ) -> ((ServerGuard, HashMap<String, Mock>), String) {
        let mut server = Server::new_async().await;
        let mut mock = server
            .mock(method, path)
            .with_status(status_code as _)
            .with_body(body);
        if let Some(content_type) = content_type {
            mock = mock.with_header("Content-Type", content_type);
        }
        let mock = mock.create_async().await;
        let url = server.url();
        (
            (server, HashMap::from([(format!("{method} {path}"), mock)])),
            url,
        )
    }

    pub async fn mock_additional(
        orig: &mut (ServerGuard, HashMap<String, Mock>),
        method: &str,
        path: &str,
        status_code: u16,
        body: Vec<u8>,
        content_type: Option<&str>,
    ) {
        let mut mock = orig
            .0
            .mock(method, path)
            .with_status(status_code as _)
            .with_body(body);
        if let Some(content_type) = content_type {
            mock = mock.with_header("Content-Type", content_type);
        }
        orig.1
            .insert(format!("{method} {path}"), mock.create_async().await);
    }

    pub async fn assert_mock((_, mocks): (ServerGuard, HashMap<String, Mock>)) {
        for mock in mocks.values() {
            mock.assert_async().await;
        }
    }

    pub async fn assert_single_mock(
        method: &str,
        path: &str,
        (_, mocks): &(ServerGuard, HashMap<String, Mock>),
    ) {
        mocks[&format!("{method} {path}")].assert_async().await;
    }

    pub async fn assert_single_mock_count(
        method: &str,
        path: &str,
        n: usize,
        (_, mocks): &mut (ServerGuard, HashMap<String, Mock>),
    ) {
        let k = format!("{method} {path}");
        let mut mock = mocks.remove(&k).unwrap();
        mock = mock.expect_at_least(n);
        mock.assert_async().await;
        mocks.insert(k, mock);
    }
}

#[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))]
mod mock {
    use js_sys::*;
    use reqwest::Client;
    use serde::Serialize;
    use std::collections::HashMap;
    use wasm_bindgen::{prelude::*, JsCast};
    use wasm_bindgen_futures::JsFuture;
    use web_sys::*;

    #[wasm_bindgen(module = "/http_mock_service_worker.js")]
    extern "C" {}

    #[derive(Debug, Serialize)]
    struct MockConfig {
        pub kind: String,
        pub method: String,
        pub path: String,
        pub status_code: u16,
        pub headers: Option<HashMap<String, String>>,
        pub body: Vec<u8>,
    }

    pub async fn mock(
        method: &str,
        path: &str,
        status_code: u16,
        body: Vec<u8>,
        content_type: Option<&str>,
    ) -> (String, String) {
        let swc = window().unwrap().navigator().service_worker();
        let registration: ServiceWorkerRegistration =
            JsFuture::from(swc.register("/http_mock_service_worker.js"))
                .await
                .unwrap()
                .unchecked_into();
        JsFuture::from(swc.ready().unwrap()).await.unwrap();
        let sw = registration.active().unwrap();
        let mut nonce = [0; 16];
        getrandom::fill(&mut nonce).unwrap();
        let nonce = hex::encode(nonce);
        let config = MockConfig {
            kind: "config".into(),
            method: method.into(),
            path: path.into(),
            status_code,
            body,
            headers: content_type.map(|c| HashMap::from([("Content-Type".into(), c.into())])),
        };
        if sw.state() == ServiceWorkerState::Activating {
            JsFuture::from(Promise::new(&mut |rs, _| sw.set_onstatechange(Some(&rs))))
                .await
                .unwrap();
        }
        Client::new()
            .post(&format!("http://mock_configure/{nonce}"))
            .json(&config)
            .send()
            .await
            .unwrap()
            .error_for_status()
            .unwrap();
        (nonce.clone(), format!("http://mock_{}/", nonce))
    }

    pub async fn mock_additional(
        orig: &mut String,
        method: &str,
        path: &str,
        status_code: u16,
        body: Vec<u8>,
        content_type: Option<&str>,
    ) {
        let config = MockConfig {
            kind: "config".into(),
            method: method.into(),
            path: path.into(),
            status_code,
            body,
            headers: content_type.map(|c| HashMap::from([("Content-Type".into(), c.into())])),
        };
        Client::new()
            .post(&format!("http://mock_configure/{orig}"))
            .json(&config)
            .send()
            .await
            .unwrap()
            .error_for_status()
            .unwrap();
    }

    async fn get_hits(nonce: &str) -> HashMap<String, i64> {
        Client::new()
            .get(&format!("http://mock_assert/{}", nonce))
            .send()
            .await
            .unwrap()
            .error_for_status()
            .unwrap()
            .json()
            .await
            .unwrap()
    }

    pub async fn assert_mock(nonce: String) {
        let hits = get_hits(&nonce).await;
        assert!(hits.values().all(|x| *x > 0));
    }

    pub async fn assert_single_mock(method: &str, path: &str, nonce: &String) {
        let hits = get_hits(nonce).await;
        assert!(hits[&format!("{method} {path}")] > 0);
    }

    pub async fn assert_single_mock_count(method: &str, path: &str, n: usize, nonce: &mut String) {
        let hits = get_hits(&*nonce).await;
        assert!(hits[&format!("{method} {path}")] >= n as i64);
    }
}
