#[cfg(feature = "host")]
use ic_query::nns::data_center::{
    DEFAULT_DATA_CENTER_REFRESH_LOCK_STALE_SECONDS, DEFAULT_NNS_DATA_CENTER_SOURCE_ENDPOINT,
    NnsDataCenterHostError, NnsDataCenterRefreshReport, NnsDataCenterRefreshRequest,
    NnsDataCenterSource, NnsDataCenterSourceRequest, build_nns_data_center_info_report,
    build_nns_data_center_info_report_with_source, build_nns_data_center_list_report,
    build_nns_data_center_list_report_with_source, nns_data_center_cache_path,
    nns_data_center_refresh_lock_path, nns_data_center_refresh_report_text,
    refresh_nns_data_center_report, refresh_nns_data_center_report_with_source,
};
use ic_query::nns::data_center::{
    NnsDataCenterCacheRequest, NnsDataCenterInfoReport, NnsDataCenterInfoRequest,
    NnsDataCenterListReport, NnsDataCenterListRequest, NnsDataCenterRow,
    nns_data_center_info_report_text, nns_data_center_list_report_text,
    nns_data_center_list_report_verbose_text,
};
#[cfg(feature = "host")]
use ic_query::nns::node::{
    DEFAULT_NNS_NODE_SOURCE_ENDPOINT, DEFAULT_NODE_REFRESH_LOCK_STALE_SECONDS, NnsNodeHostError,
    NnsNodeRefreshReport, NnsNodeRefreshRequest, NnsNodeSource, NnsNodeSourceRequest,
    build_nns_node_info_report, build_nns_node_info_report_with_source, build_nns_node_list_report,
    build_nns_node_list_report_with_source, nns_node_cache_path, nns_node_refresh_lock_path,
    nns_node_refresh_report_text, refresh_nns_node_report, refresh_nns_node_report_with_source,
};
use ic_query::nns::node::{
    NNS_NODE_SUBNET_KIND_APPLICATION, NnsNodeCacheRequest, NnsNodeInfoReport, NnsNodeInfoRequest,
    NnsNodeListReport, NnsNodeListRequest, NnsNodeRow, nns_node_info_report_text,
    nns_node_list_report_text, nns_node_list_report_verbose_text,
};
#[cfg(feature = "host")]
use ic_query::nns::node_operator::{
    DEFAULT_NNS_NODE_OPERATOR_SOURCE_ENDPOINT, DEFAULT_NODE_OPERATOR_REFRESH_LOCK_STALE_SECONDS,
    NnsNodeOperatorHostError, NnsNodeOperatorRefreshReport, NnsNodeOperatorRefreshRequest,
    NnsNodeOperatorSource, NnsNodeOperatorSourceRequest, build_nns_node_operator_info_report,
    build_nns_node_operator_info_report_with_source, build_nns_node_operator_list_report,
    build_nns_node_operator_list_report_with_source, nns_node_operator_cache_path,
    nns_node_operator_refresh_lock_path, nns_node_operator_refresh_report_text,
    refresh_nns_node_operator_report, refresh_nns_node_operator_report_with_source,
};
use ic_query::nns::node_operator::{
    NnsNodeOperatorCacheRequest, NnsNodeOperatorInfoReport, NnsNodeOperatorInfoRequest,
    NnsNodeOperatorListReport, NnsNodeOperatorListRequest, NnsNodeOperatorRow,
    nns_node_operator_info_report_text, nns_node_operator_list_report_text,
    nns_node_operator_list_report_verbose_text,
};
#[cfg(feature = "host")]
use ic_query::nns::node_provider::{
    DEFAULT_NNS_SOURCE_ENDPOINT, DEFAULT_NODE_PROVIDER_REFRESH_LOCK_STALE_SECONDS,
    NnsNodeProviderHostError, NnsNodeProviderRefreshReport, NnsNodeProviderRefreshRequest,
    NnsNodeProviderSource, NnsNodeProviderSourceRequest, build_nns_node_provider_info_report,
    build_nns_node_provider_info_report_with_source, build_nns_node_provider_list_report,
    build_nns_node_provider_list_report_with_source, nns_node_provider_cache_path,
    nns_node_provider_refresh_lock_path, nns_node_provider_refresh_report_text,
    refresh_nns_node_provider_report, refresh_nns_node_provider_report_with_source,
};
use ic_query::nns::node_provider::{
    NnsNodeProviderCacheRequest, NnsNodeProviderInfoReport, NnsNodeProviderInfoRequest,
    NnsNodeProviderListReport, NnsNodeProviderListRequest, NnsNodeProviderRow,
    nns_node_provider_info_report_text, nns_node_provider_list_report_text,
    nns_node_provider_list_report_verbose_text,
};
#[cfg(feature = "host")]
use ic_query::nns::proposals::{
    DEFAULT_NNS_PROPOSAL_REFRESH_LOCK_STALE_SECONDS, DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT,
    NnsProposalCacheListRequest, NnsProposalCacheStatusRequest, NnsProposalHostError,
    NnsProposalRefreshReport, NnsProposalRefreshRequest, NnsProposalSource,
    NnsProposalSourceRequest, build_nns_proposal_cache_list_report,
    build_nns_proposal_cache_status_report, build_nns_proposal_list_report,
    build_nns_proposal_list_report_from_cache, build_nns_proposal_list_report_with_source,
    build_nns_proposal_report, build_nns_proposal_report_from_cache,
    build_nns_proposal_report_with_source, nns_proposal_cache_list_report_text,
    nns_proposal_cache_path, nns_proposal_cache_root, nns_proposal_cache_status_report_text,
    nns_proposal_refresh_attempt_path, nns_proposal_refresh_lock_path,
    nns_proposal_refresh_report_text, refresh_nns_proposal_cache,
    refresh_nns_proposal_cache_with_source,
};
use ic_query::nns::proposals::{
    NnsProposalBallotRow, NnsProposalListReport, NnsProposalListRequest, NnsProposalListSort,
    NnsProposalReport, NnsProposalRequest, NnsProposalRewardStatusFilter, NnsProposalRow,
    NnsProposalSortDirection, NnsProposalStatusFilter, NnsProposalTally, NnsProposalTopicFilter,
    nns_proposal_list_report_text, nns_proposal_report_text,
};
#[cfg(feature = "host")]
use ic_query::nns::registry::{
    NnsRegistryHostError, NnsRegistrySource, NnsRegistrySourceRequest, NnsRegistryVersionData,
    build_nns_registry_version_report_with_source,
};
use ic_query::nns::registry::{
    NnsRegistryVersionReport, NnsRegistryVersionRequest, nns_registry_version_report_text,
};
use ic_query::nns::topology::report::{
    NnsTopologyCapacityReport, NnsTopologyCapacityRow, NnsTopologyCoverageReport,
    NnsTopologyGapRow, NnsTopologyGapsReport, NnsTopologyHealthCheckRow, NnsTopologyHealthReport,
    NnsTopologyProviderRow, NnsTopologyProvidersReport, NnsTopologyRefreshReport,
    NnsTopologyRefreshRequest, NnsTopologyRefreshRow, NnsTopologyRegionRow,
    NnsTopologyRegionsReport, NnsTopologyRegistryVersionRow, NnsTopologySummaryReport,
    NnsTopologySummaryRequest, NnsTopologyVersionsReport, nns_topology_capacity_report_text,
    nns_topology_coverage_report_text, nns_topology_gaps_report_text,
    nns_topology_health_report_text, nns_topology_providers_report_text,
    nns_topology_refresh_report_text, nns_topology_regions_report_text,
    nns_topology_summary_report_text, nns_topology_versions_report_text,
};
#[cfg(feature = "host")]
use serde::Serialize;
#[cfg(feature = "host")]
use std::{
    fs,
    path::{Path, PathBuf},
};

#[test]
fn public_nns_registry_api_is_constructible_and_renderable() {
    let request = NnsRegistryVersionRequest::new("ic", "https://icp-api.io", 1_700_000_000);

    assert_eq!(request.network, "ic");

    let report = NnsRegistryVersionReport {
        schema_version: 1,
        network: request.network,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
    };

    let text = nns_registry_version_report_text(&report);

    assert!(text.contains("network: ic"));
    assert!(text.contains("registry_version: 42"));
}

#[cfg(feature = "host")]
#[test]
fn public_nns_registry_host_api_accepts_custom_source_adapter() {
    let request = NnsRegistryVersionRequest::new("ic", "https://icp-api.io", 1_700_000_000);
    let report = build_nns_registry_version_report_with_source(&request, &FixtureNnsRegistrySource)
        .expect("registry version report");

    assert_eq!(report.network, "ic");
    assert_eq!(report.registry_canister_id, "rwlgt-iiaaa-aaaaa-aaaaa-cai");
    assert_eq!(report.registry_version, 42);
    assert_eq!(report.source_endpoint, "https://icp-api.io");
}

#[cfg(feature = "host")]
struct FixtureNnsRegistrySource;

#[cfg(feature = "host")]
impl NnsRegistrySource for FixtureNnsRegistrySource {
    fn fetch_registry_version(
        &self,
        request: &NnsRegistrySourceRequest,
    ) -> Result<NnsRegistryVersionData, NnsRegistryHostError> {
        assert_eq!(request.endpoint, "https://icp-api.io");
        assert_eq!(request.fetched_by, "ic-query");
        assert!(!request.fetched_at.is_empty());

        Ok(NnsRegistryVersionData {
            network: "ic".to_string(),
            registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
            registry_version: 42,
            fetched_at: request.fetched_at.clone(),
            fetched_by: request.fetched_by.clone(),
            source_endpoint: request.endpoint.clone(),
        })
    }
}

#[test]
fn public_nns_node_api_is_constructible_and_renderable() {
    let cache = NnsNodeCacheRequest::new(".", "ic");
    let list_request = NnsNodeListRequest::new(cache.clone(), "https://icp-api.io", 1_700_000_000)
        .with_subnet("tdb26-jop6g")
        .with_subnet_kind(NNS_NODE_SUBNET_KIND_APPLICATION)
        .with_data_center("zh1");

    assert_eq!(
        list_request.filters.subnet_kind.as_deref(),
        Some(NNS_NODE_SUBNET_KIND_APPLICATION)
    );

    let node = sample_nns_node_row();
    let list_report = NnsNodeListReport {
        schema_version: 1,
        network: list_request.cache.network,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: list_request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        node_count: 1,
        nodes: vec![node.clone()],
    };

    let list_text = nns_node_list_report_text(&list_report);
    let verbose_text = nns_node_list_report_verbose_text(&list_report);

    assert!(list_text.contains("nodes: ic count 1"));
    assert!(list_text.contains(NNS_NODE_SUBNET_KIND_APPLICATION));
    assert!(verbose_text.contains("source_endpoint: https://icp-api.io"));
    assert!(verbose_text.contains("tdb26-jop6g-7sc54-foywl"));

    let info_request = NnsNodeInfoRequest::new(
        cache,
        "https://icp-api.io",
        node.node_principal.clone(),
        1_700_000_000,
    );
    let info_report = NnsNodeInfoReport {
        schema_version: 1,
        input: info_request.input,
        resolved_from: "node_principal".to_string(),
        network: info_request.cache.network,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: info_request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        node_principal: node.node_principal,
        node_operator_principal: node.node_operator_principal,
        node_provider_principal: node.node_provider_principal,
        subnet_principal: node.subnet_principal,
        subnet_kind: node.subnet_kind,
        data_center_id: node.data_center_id,
    };

    let info_text = nns_node_info_report_text(&info_report);

    assert!(info_text.contains("resolved_from: node_principal"));
    assert!(info_text.contains("data_center_id: zh1"));
}

#[test]
fn public_nns_data_center_api_is_constructible_and_renderable() {
    let cache = NnsDataCenterCacheRequest::new(".", "ic");
    let request = NnsDataCenterListRequest::new(cache.clone(), "https://icp-api.io", 1_700_000_000);
    let data_center = sample_nns_data_center_row();
    let list = NnsDataCenterListReport {
        schema_version: 1,
        network: request.cache.network,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        data_center_count: 1,
        data_centers: vec![data_center.clone()],
    };

    let text = nns_data_center_list_report_text(&list);
    let verbose_text = nns_data_center_list_report_verbose_text(&list);

    assert!(text.contains("data_centers: ic count 1"));
    assert!(text.contains("Zurich"));
    assert!(verbose_text.contains("REGISTRY_VERSION"));

    let info_request = NnsDataCenterInfoRequest::new(
        cache,
        "https://icp-api.io",
        data_center.data_center_id.clone(),
        1_700_000_000,
    );
    let info = NnsDataCenterInfoReport {
        schema_version: 1,
        input: info_request.input,
        resolved_from: "data_center_id".to_string(),
        network: info_request.cache.network,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: info_request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        data_center_id: data_center.data_center_id,
        region: data_center.region,
        owner: data_center.owner,
        latitude: data_center.latitude,
        longitude: data_center.longitude,
        node_operator_count: data_center.node_operator_count,
        node_provider_count: data_center.node_provider_count,
        node_count: data_center.node_count,
    };

    let info_text = nns_data_center_info_report_text(&info);

    assert!(info_text.contains("resolved_from: data_center_id"));
    assert!(info_text.contains("node_count: 12"));
}

#[test]
fn public_nns_node_provider_api_is_constructible_and_renderable() {
    let cache = NnsNodeProviderCacheRequest::new(".", "ic");
    let request =
        NnsNodeProviderListRequest::new(cache.clone(), "https://icp-api.io", 1_700_000_000);
    let provider = sample_nns_node_provider_row();
    let list = NnsNodeProviderListReport {
        schema_version: 1,
        network: request.cache.network,
        governance_canister_id: "rrkah-fqaaa-aaaaa-aaaaq-cai".to_string(),
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        node_provider_count: 1,
        node_providers: vec![provider.clone()],
    };

    let text = nns_node_provider_list_report_text(&list);
    let verbose_text = nns_node_provider_list_report_verbose_text(&list);

    assert!(text.contains("node_providers: ic count 1"));
    assert!(text.contains("12"));
    assert!(verbose_text.contains("deadbeef"));

    let info_request = NnsNodeProviderInfoRequest::new(
        cache,
        "https://icp-api.io",
        provider.node_provider_principal.clone(),
        1_700_000_000,
    );
    let info = NnsNodeProviderInfoReport {
        schema_version: 1,
        input: info_request.input,
        resolved_from: "node_provider_principal".to_string(),
        network: info_request.cache.network,
        governance_canister_id: "rrkah-fqaaa-aaaaa-aaaaq-cai".to_string(),
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: info_request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        node_provider_principal: provider.node_provider_principal,
        name: provider.name,
        node_count: provider.node_count,
        reward_account_hex: provider.reward_account_hex,
    };

    let info_text = nns_node_provider_info_report_text(&info);

    assert!(info_text.contains("resolved_from: node_provider_principal"));
    assert!(info_text.contains("reward_account_hex: deadbeef"));
}

#[test]
fn public_nns_node_operator_api_is_constructible_and_renderable() {
    let cache = NnsNodeOperatorCacheRequest::new(".", "ic");
    let request =
        NnsNodeOperatorListRequest::new(cache.clone(), "https://icp-api.io", 1_700_000_000);
    let operator = sample_nns_node_operator_row();
    let list = NnsNodeOperatorListReport {
        schema_version: 1,
        network: request.cache.network,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        node_operator_count: 1,
        node_operators: vec![operator.clone()],
    };

    let text = nns_node_operator_list_report_text(&list);
    let verbose_text = nns_node_operator_list_report_verbose_text(&list);

    assert!(text.contains("node_operators: ic count 1"));
    assert!(text.contains("zh1"));
    assert!(verbose_text.contains("tdb26-jop6g-7sc54-foywl"));

    let info_request = NnsNodeOperatorInfoRequest::new(
        cache,
        "https://icp-api.io",
        operator.node_operator_principal.clone(),
        1_700_000_000,
    );
    let info = NnsNodeOperatorInfoReport {
        schema_version: 1,
        input: info_request.input,
        resolved_from: "node_operator_principal".to_string(),
        network: info_request.cache.network,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: info_request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        node_operator_principal: operator.node_operator_principal,
        node_provider_principal: operator.node_provider_principal,
        node_allowance: operator.node_allowance,
        data_center_id: operator.data_center_id,
        node_count: operator.node_count,
    };

    let info_text = nns_node_operator_info_report_text(&info);

    assert!(info_text.contains("resolved_from: node_operator_principal"));
    assert!(info_text.contains("node_allowance: 28"));
}

#[cfg(feature = "host")]
#[test]
fn public_nns_inventory_host_api_reads_cached_reports_without_cli() {
    let root = temp_root("nns-inventory-host-public-api");
    write_nns_inventory_fixture_caches(&root);

    assert_public_nns_node_host_api(&root);
    assert_public_nns_data_center_host_api(&root);
    assert_public_nns_node_provider_host_api(&root);
    assert_public_nns_node_operator_host_api(&root);

    let _ = fs::remove_dir_all(root);
}

#[cfg(feature = "host")]
#[test]
fn public_nns_inventory_host_api_accepts_custom_source_adapters() {
    let root = temp_root("nns-inventory-source-public-api");

    assert_public_nns_node_custom_source_api(&root);
    assert_public_nns_data_center_custom_source_api(&root);
    assert_public_nns_node_provider_custom_source_api(&root);
    assert_public_nns_node_operator_custom_source_api(&root);

    let _ = fs::remove_dir_all(root);
}

#[cfg(feature = "host")]
fn assert_public_nns_node_custom_source_api(root: &Path) {
    let node_cache = NnsNodeCacheRequest::new(root.join("node"), "ic");
    let node_list_request = NnsNodeListRequest::new(
        node_cache.clone(),
        DEFAULT_NNS_NODE_SOURCE_ENDPOINT,
        1_700_000_000,
    );
    let node_info_request = NnsNodeInfoRequest::new(
        node_cache.clone(),
        DEFAULT_NNS_NODE_SOURCE_ENDPOINT,
        sample_nns_node_row().node_principal,
        1_700_000_000,
    );
    let node_refresh_request = NnsNodeRefreshRequest::new(
        node_cache,
        DEFAULT_NNS_NODE_SOURCE_ENDPOINT,
        1_700_000_000,
        DEFAULT_NODE_REFRESH_LOCK_STALE_SECONDS,
    )
    .with_dry_run(true);
    let node_list =
        build_nns_node_list_report_with_source(&node_list_request, &FixtureNnsNodeSource)
            .expect("node list report");
    let node_info =
        build_nns_node_info_report_with_source(&node_info_request, &FixtureNnsNodeSource)
            .expect("node info report");
    let node_refresh =
        refresh_nns_node_report_with_source(&node_refresh_request, &FixtureNnsNodeSource)
            .expect("node refresh report");

    assert_eq!(node_list.node_count, 1);
    assert_eq!(
        node_info.node_principal,
        sample_nns_node_row().node_principal
    );
    assert_eq!(node_refresh.node_count, 1);
}

#[cfg(feature = "host")]
fn assert_public_nns_data_center_custom_source_api(root: &Path) {
    let data_center_cache = NnsDataCenterCacheRequest::new(root.join("data-center"), "ic");
    let data_center_list_request = NnsDataCenterListRequest::new(
        data_center_cache.clone(),
        DEFAULT_NNS_DATA_CENTER_SOURCE_ENDPOINT,
        1_700_000_000,
    );
    let data_center_info_request = NnsDataCenterInfoRequest::new(
        data_center_cache.clone(),
        DEFAULT_NNS_DATA_CENTER_SOURCE_ENDPOINT,
        sample_nns_data_center_row().data_center_id,
        1_700_000_000,
    );
    let data_center_refresh_request = NnsDataCenterRefreshRequest::new(
        data_center_cache,
        DEFAULT_NNS_DATA_CENTER_SOURCE_ENDPOINT,
        1_700_000_000,
        DEFAULT_DATA_CENTER_REFRESH_LOCK_STALE_SECONDS,
    )
    .with_dry_run(true);
    let data_center_list = build_nns_data_center_list_report_with_source(
        &data_center_list_request,
        &FixtureNnsDataCenterSource,
    )
    .expect("data-center list report");
    let data_center_info = build_nns_data_center_info_report_with_source(
        &data_center_info_request,
        &FixtureNnsDataCenterSource,
    )
    .expect("data-center info report");
    let data_center_refresh = refresh_nns_data_center_report_with_source(
        &data_center_refresh_request,
        &FixtureNnsDataCenterSource,
    )
    .expect("data-center refresh report");

    assert_eq!(data_center_list.data_center_count, 1);
    assert_eq!(
        data_center_info.data_center_id,
        sample_nns_data_center_row().data_center_id
    );
    assert_eq!(data_center_refresh.data_center_count, 1);
}

#[cfg(feature = "host")]
fn assert_public_nns_node_provider_custom_source_api(root: &Path) {
    let node_provider_cache = NnsNodeProviderCacheRequest::new(root.join("node-provider"), "ic");
    let node_provider_list_request = NnsNodeProviderListRequest::new(
        node_provider_cache.clone(),
        DEFAULT_NNS_SOURCE_ENDPOINT,
        1_700_000_000,
    );
    let node_provider_info_request = NnsNodeProviderInfoRequest::new(
        node_provider_cache.clone(),
        DEFAULT_NNS_SOURCE_ENDPOINT,
        sample_nns_node_provider_row().node_provider_principal,
        1_700_000_000,
    );
    let node_provider_refresh_request = NnsNodeProviderRefreshRequest::new(
        node_provider_cache,
        DEFAULT_NNS_SOURCE_ENDPOINT,
        1_700_000_000,
        DEFAULT_NODE_PROVIDER_REFRESH_LOCK_STALE_SECONDS,
    )
    .with_dry_run(true);
    let node_provider_list = build_nns_node_provider_list_report_with_source(
        &node_provider_list_request,
        &FixtureNnsNodeProviderSource,
    )
    .expect("node-provider list report");
    let node_provider_info = build_nns_node_provider_info_report_with_source(
        &node_provider_info_request,
        &FixtureNnsNodeProviderSource,
    )
    .expect("node-provider info report");
    let node_provider_refresh = refresh_nns_node_provider_report_with_source(
        &node_provider_refresh_request,
        &FixtureNnsNodeProviderSource,
    )
    .expect("node-provider refresh report");

    assert_eq!(node_provider_list.node_provider_count, 1);
    assert_eq!(
        node_provider_info.node_provider_principal,
        sample_nns_node_provider_row().node_provider_principal
    );
    assert_eq!(node_provider_refresh.node_provider_count, 1);
}

#[cfg(feature = "host")]
fn assert_public_nns_node_operator_custom_source_api(root: &Path) {
    let node_operator_cache = NnsNodeOperatorCacheRequest::new(root.join("node-operator"), "ic");
    let node_operator_list_request = NnsNodeOperatorListRequest::new(
        node_operator_cache.clone(),
        DEFAULT_NNS_NODE_OPERATOR_SOURCE_ENDPOINT,
        1_700_000_000,
    );
    let node_operator_info_request = NnsNodeOperatorInfoRequest::new(
        node_operator_cache.clone(),
        DEFAULT_NNS_NODE_OPERATOR_SOURCE_ENDPOINT,
        sample_nns_node_operator_row().node_operator_principal,
        1_700_000_000,
    );
    let node_operator_refresh_request = NnsNodeOperatorRefreshRequest::new(
        node_operator_cache,
        DEFAULT_NNS_NODE_OPERATOR_SOURCE_ENDPOINT,
        1_700_000_000,
        DEFAULT_NODE_OPERATOR_REFRESH_LOCK_STALE_SECONDS,
    )
    .with_dry_run(true);
    let node_operator_list = build_nns_node_operator_list_report_with_source(
        &node_operator_list_request,
        &FixtureNnsNodeOperatorSource,
    )
    .expect("node-operator list report");
    let node_operator_info = build_nns_node_operator_info_report_with_source(
        &node_operator_info_request,
        &FixtureNnsNodeOperatorSource,
    )
    .expect("node-operator info report");
    let node_operator_refresh = refresh_nns_node_operator_report_with_source(
        &node_operator_refresh_request,
        &FixtureNnsNodeOperatorSource,
    )
    .expect("node-operator refresh report");

    assert_eq!(node_operator_list.node_operator_count, 1);
    assert_eq!(
        node_operator_info.node_operator_principal,
        sample_nns_node_operator_row().node_operator_principal
    );
    assert_eq!(node_operator_refresh.node_operator_count, 1);
}

#[cfg(feature = "host")]
struct FixtureNnsNodeSource;

#[cfg(feature = "host")]
impl NnsNodeSource for FixtureNnsNodeSource {
    fn fetch_node_list_report(
        &self,
        request: &NnsNodeSourceRequest,
    ) -> Result<NnsNodeListReport, NnsNodeHostError> {
        assert_inventory_source_request(
            &request.network,
            &request.endpoint,
            &request.fetched_at,
            &request.fetched_by,
        );
        let mut report = sample_nns_node_list_report();
        report.network.clone_from(&request.network);
        report.fetched_at.clone_from(&request.fetched_at);
        report.source_endpoint.clone_from(&request.endpoint);
        report.fetched_by.clone_from(&request.fetched_by);
        Ok(report)
    }
}

#[cfg(feature = "host")]
struct FixtureNnsDataCenterSource;

#[cfg(feature = "host")]
impl NnsDataCenterSource for FixtureNnsDataCenterSource {
    fn fetch_data_center_list_report(
        &self,
        request: &NnsDataCenterSourceRequest,
    ) -> Result<NnsDataCenterListReport, NnsDataCenterHostError> {
        assert_inventory_source_request(
            &request.network,
            &request.endpoint,
            &request.fetched_at,
            &request.fetched_by,
        );
        let mut report = sample_nns_data_center_list_report();
        report.network.clone_from(&request.network);
        report.fetched_at.clone_from(&request.fetched_at);
        report.source_endpoint.clone_from(&request.endpoint);
        report.fetched_by.clone_from(&request.fetched_by);
        Ok(report)
    }
}

#[cfg(feature = "host")]
struct FixtureNnsNodeProviderSource;

#[cfg(feature = "host")]
impl NnsNodeProviderSource for FixtureNnsNodeProviderSource {
    fn fetch_node_provider_list_report(
        &self,
        request: &NnsNodeProviderSourceRequest,
    ) -> Result<NnsNodeProviderListReport, NnsNodeProviderHostError> {
        assert_inventory_source_request(
            &request.network,
            &request.endpoint,
            &request.fetched_at,
            &request.fetched_by,
        );
        let mut report = sample_nns_node_provider_list_report();
        report.network.clone_from(&request.network);
        report.fetched_at.clone_from(&request.fetched_at);
        report.source_endpoint.clone_from(&request.endpoint);
        report.fetched_by.clone_from(&request.fetched_by);
        Ok(report)
    }
}

#[cfg(feature = "host")]
struct FixtureNnsNodeOperatorSource;

#[cfg(feature = "host")]
impl NnsNodeOperatorSource for FixtureNnsNodeOperatorSource {
    fn fetch_node_operator_list_report(
        &self,
        request: &NnsNodeOperatorSourceRequest,
    ) -> Result<NnsNodeOperatorListReport, NnsNodeOperatorHostError> {
        assert_inventory_source_request(
            &request.network,
            &request.endpoint,
            &request.fetched_at,
            &request.fetched_by,
        );
        let mut report = sample_nns_node_operator_list_report();
        report.network.clone_from(&request.network);
        report.fetched_at.clone_from(&request.fetched_at);
        report.source_endpoint.clone_from(&request.endpoint);
        report.fetched_by.clone_from(&request.fetched_by);
        Ok(report)
    }
}

#[cfg(feature = "host")]
fn assert_inventory_source_request(
    network: &str,
    endpoint: &str,
    fetched_at: &str,
    fetched_by: &str,
) {
    assert_eq!(network, "ic");
    assert_eq!(endpoint, "https://icp-api.io");
    assert!(!fetched_at.is_empty());
    assert_eq!(fetched_by, "ic-query");
}

#[test]
fn public_nns_proposal_api_is_constructible_and_renderable() {
    let request = NnsProposalListRequest::new("ic", "https://icp-api.io", 1_700_000_000, 25)
        .with_before_proposal_id(132_500)
        .with_status(NnsProposalStatusFilter::Executed)
        .with_reward_status(NnsProposalRewardStatusFilter::Settled)
        .with_topic(NnsProposalTopicFilter::Governance)
        .with_proposer_neuron_id(12_345)
        .with_query("subnet")
        .with_sort(NnsProposalListSort::TallyTime)
        .with_sort_direction(NnsProposalSortDirection::Desc)
        .with_verbose(true);

    assert_eq!(request.status.as_str(), "executed");
    assert_eq!(request.reward_status.as_str(), "settled");
    assert_eq!(request.topic.as_str(), "governance");
    assert_eq!(request.sort.direction_label(request.sort_direction), "desc");

    let proposal = sample_nns_proposal_row();
    let list_report = NnsProposalListReport {
        schema_version: 3,
        network: request.network,
        governance_canister_id: "rrkah-fqaaa-aaaaa-aaaaq-cai".to_string(),
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        data_source: "cache".to_string(),
        cache_path: Some(".icq/nns/ic/governance/proposals/full.json".to_string()),
        cache_complete: Some(true),
        requested_limit: request.limit,
        before_proposal_id: request.before_proposal_id,
        status_filter: request.status.as_str().to_string(),
        reward_status_filter: request.reward_status.as_str().to_string(),
        topic_filter: request.topic.as_str().to_string(),
        proposer_filter: request.proposer_neuron_id,
        query_filter: request.query,
        sort: request.sort.as_str().to_string(),
        sort_direction: request
            .sort
            .direction_label(request.sort_direction)
            .to_string(),
        result_scope: "complete-cache".to_string(),
        verbose: request.verbose,
        proposal_count: 1,
        proposals: vec![proposal.clone()],
    };

    let list_text = nns_proposal_list_report_text(&list_report);

    assert!(list_text.contains("proposal_count: 1"));
    assert!(list_text.contains("topic_filter: governance"));
    assert!(list_text.contains("proposal_details:"));
    assert!(list_text.contains("title: Upgrade subnet"));

    let detail_request =
        NnsProposalRequest::new("ic", "https://icp-api.io", 1_700_000_000, 132_411)
            .with_show_ballots(true);
    let detail_report = NnsProposalReport {
        schema_version: 1,
        network: detail_request.network,
        governance_canister_id: "rrkah-fqaaa-aaaaa-aaaaq-cai".to_string(),
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: detail_request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        data_source: "live".to_string(),
        cache_path: None,
        cache_complete: None,
        proposal_id: detail_request.proposal_id,
        show_ballots: detail_request.show_ballots,
        verbose: detail_request.verbose,
        proposal,
    };

    let detail_text = nns_proposal_report_text(&detail_report);

    assert!(detail_text.contains("proposal_id: 132411"));
    assert!(detail_text.contains("show_ballots: yes"));
    assert!(detail_text.contains("reject_cost: 1.00"));
    assert!(detail_text.contains("ballots:"));
    assert!(detail_text.contains("yes"));
}

#[test]
fn public_nns_topology_summary_and_versions_api_is_constructible_and_renderable() {
    let request = NnsTopologySummaryRequest::new(".", "ic", "https://icp-api.io", 1_700_000_000);
    assert_eq!(request.network, "ic");

    let registry_version = sample_topology_registry_version_row();
    let summary = NnsTopologySummaryReport {
        schema_version: 3,
        network: request.network,
        source_endpoint: request.source_endpoint,
        subnet_count: 2,
        application_subnet_count: 1,
        cloud_engine_subnet_count: 0,
        system_subnet_count: 1,
        unknown_subnet_count: 0,
        routing_range_count: 4,
        node_count: 3,
        application_node_count: 2,
        cloud_engine_node_count: 0,
        system_node_count: 1,
        unknown_node_count: 0,
        node_provider_count: 1,
        node_operator_count: 1,
        data_center_count: 1,
        nodes_with_known_node_provider_count: 3,
        nodes_with_unknown_node_provider_count: 0,
        nodes_with_known_node_operator_count: 3,
        nodes_with_unknown_node_operator_count: 0,
        nodes_with_known_data_center_count: 3,
        nodes_with_unknown_data_center_count: 0,
        node_operators_with_known_node_provider_count: 1,
        node_operators_with_unknown_node_provider_count: 0,
        node_operators_with_known_data_center_count: 1,
        node_operators_with_unknown_data_center_count: 0,
        subnet_catalog_stale: false,
        subnet_catalog_stale_reason: "fresh".to_string(),
        registry_versions: vec![registry_version.clone()],
    };
    let summary_text = nns_topology_summary_report_text(&summary);
    assert!(summary_text.contains("topology: ic subnets 2 nodes 3"));
    assert!(summary_text.contains("subnet_catalog"));

    let versions = NnsTopologyVersionsReport {
        schema_version: 1,
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        source_count: 1,
        registry_versions: vec![registry_version],
    };
    assert!(nns_topology_versions_report_text(&versions).contains("subnet_catalog"));
}

#[test]
fn public_nns_topology_coverage_and_health_api_is_constructible_and_renderable() {
    let health = NnsTopologyHealthReport {
        schema_version: 1,
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        status: "ok".to_string(),
        registry_source_count: 1,
        registry_version_min: Some(42),
        registry_version_max: Some(42),
        registry_versions_aligned: true,
        stale_source_count: 0,
        subnet_catalog_stale: false,
        subnet_catalog_stale_reason: "fresh".to_string(),
        known_join_count: 11,
        unknown_join_count: 0,
        join_coverage: "100.0%".to_string(),
        checks: vec![NnsTopologyHealthCheckRow {
            check: "registry_versions".to_string(),
            status: "ok".to_string(),
            detail: "1 source at registry version 42".to_string(),
        }],
    };
    assert!(nns_topology_health_report_text(&health).contains("registry_versions"));

    let coverage = NnsTopologyCoverageReport {
        schema_version: 1,
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        node_count: 3,
        node_provider_count: 1,
        node_operator_count: 1,
        data_center_count: 1,
        nodes_with_known_node_provider_count: 3,
        nodes_with_unknown_node_provider_count: 0,
        nodes_with_known_node_operator_count: 3,
        nodes_with_unknown_node_operator_count: 0,
        nodes_with_known_data_center_count: 3,
        nodes_with_unknown_data_center_count: 0,
        node_operators_with_known_node_provider_count: 1,
        node_operators_with_unknown_node_provider_count: 0,
        node_operators_with_known_data_center_count: 1,
        node_operators_with_unknown_data_center_count: 0,
    };
    assert!(nns_topology_coverage_report_text(&coverage).contains("nodes"));
}

#[test]
fn public_nns_topology_gaps_and_capacity_api_is_constructible_and_renderable() {
    let gaps = NnsTopologyGapsReport {
        schema_version: 1,
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        status: "attention".to_string(),
        gap_count: 1,
        gaps: vec![NnsTopologyGapRow {
            subject_kind: "node".to_string(),
            subject: "zh3jp-xqaaa-aaaar-qaada-cai".to_string(),
            missing_relation: "node_operator".to_string(),
            referenced_id: "qoctq-giaaa-aaaar-qaada-cai".to_string(),
        }],
    };
    assert!(nns_topology_gaps_report_text(&gaps).contains("node_operator"));

    let capacity = NnsTopologyCapacityReport {
        schema_version: 1,
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        status: "attention".to_string(),
        node_operator_count: 1,
        total_node_allowance: 2,
        assigned_node_count: 3,
        unknown_node_count_operator_count: 0,
        available_node_slots: 0,
        over_assigned_operator_count: 1,
        over_assigned_node_count: 1,
        capacity: vec![NnsTopologyCapacityRow {
            node_operator_principal: "qoctq-giaaa-aaaar-qaada-cai".to_string(),
            node_provider_principal: "w6gnz-6qaaa-aaaar-qaada-cai".to_string(),
            data_center_id: "zh1".to_string(),
            node_allowance: 2,
            assigned_node_count: Some(3),
            available_node_slots: Some(0),
            over_assigned_node_count: Some(1),
            utilization: "150.0%".to_string(),
            status: "over".to_string(),
        }],
    };
    assert!(nns_topology_capacity_report_text(&capacity).contains("over"));
}

#[test]
fn public_nns_topology_region_provider_and_refresh_api_is_constructible_and_renderable() {
    let regions = NnsTopologyRegionsReport {
        schema_version: 1,
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        region_count: 1,
        data_center_count: 1,
        node_operator_count: 1,
        node_provider_count: 1,
        node_count: 3,
        regions: vec![NnsTopologyRegionRow {
            region: "Zurich".to_string(),
            data_center_count: 1,
            node_operator_count: 1,
            node_provider_count: 1,
            node_count: 3,
        }],
    };
    assert!(nns_topology_regions_report_text(&regions).contains("Zurich"));

    let providers = NnsTopologyProvidersReport {
        schema_version: 1,
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        registered_node_provider_count: 1,
        referenced_node_provider_count: 1,
        provider_with_nodes_count: 1,
        provider_with_node_operators_count: 1,
        total_node_count: 3,
        total_node_operator_count: 1,
        total_node_allowance: 2,
        over_assigned_provider_count: 1,
        unknown_provider_count: 0,
        providers: vec![NnsTopologyProviderRow {
            node_provider_principal: "w6gnz-6qaaa-aaaar-qaada-cai".to_string(),
            registered: true,
            name: Some("Example Provider".to_string()),
            governance_node_count: Some(3),
            topology_node_count: 3,
            node_operator_count: 1,
            data_center_count: 1,
            region_count: 1,
            total_node_allowance: 2,
            assigned_node_count: 3,
            available_node_slots: 0,
            over_assigned_node_count: 1,
            status: "over".to_string(),
        }],
    };
    assert!(nns_topology_providers_report_text(&providers).contains("over"));

    let refresh_request =
        NnsTopologyRefreshRequest::new(".", "ic", "https://icp-api.io", 1_700_000_000, 1_800)
            .with_dry_run(true);
    let refresh = NnsTopologyRefreshReport {
        schema_version: 1,
        network: refresh_request.network,
        source_endpoint: refresh_request.source_endpoint,
        dry_run: refresh_request.dry_run,
        component_count: 1,
        wrote_cache_count: 0,
        replaced_existing_cache_count: 0,
        components: vec![NnsTopologyRefreshRow {
            source: "subnet_catalog".to_string(),
            cache_path: ".icq/subnet-catalog/ic/catalog.json".to_string(),
            refresh_lock_path: ".icq/subnet-catalog/ic/refresh.lock".to_string(),
            registry_version: 42,
            fetched_at: "2023-11-14T22:13:20Z".to_string(),
            source_endpoint: "https://icp-api.io".to_string(),
            fetched_by: "ic-query".to_string(),
            dry_run: true,
            wrote_cache: false,
            replaced_existing_cache: false,
            item_count: 2,
        }],
    };
    assert!(nns_topology_refresh_report_text(&refresh).contains("topology_refresh: ic"));
}

#[cfg(feature = "host")]
type RefreshFn<Request, Report, Error> = fn(&Request) -> Result<Report, Error>;

#[cfg(feature = "host")]
type RequestReportFn<Request, Report, Error> = fn(&Request) -> Result<Report, Error>;

#[cfg(feature = "host")]
#[test]
fn public_nns_proposal_host_api_reads_complete_cache_without_cli() {
    let root = temp_root("nns-proposal-host-public-api");
    write_nns_proposal_fixture_cache(&root);

    let cache_list_request = NnsProposalCacheListRequest::new(&root, "ic");
    let cache_status_request = NnsProposalCacheStatusRequest::new(&root, "ic");
    let list_request = NnsProposalListRequest::new(
        "ic",
        DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT,
        1_700_000_000,
        25,
    )
    .with_status(NnsProposalStatusFilter::Executed)
    .with_reward_status(NnsProposalRewardStatusFilter::Settled)
    .with_topic(NnsProposalTopicFilter::Governance)
    .with_proposer_neuron_id(12_345)
    .with_query("subnet")
    .with_sort(NnsProposalListSort::TallyTime)
    .with_sort_direction(NnsProposalSortDirection::Desc);
    let detail_request = NnsProposalRequest::new(
        "ic",
        DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT,
        1_700_000_000,
        132_411,
    )
    .with_show_ballots(true);
    let refresh_request = NnsProposalRefreshRequest::new(
        &root,
        "ic",
        DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT,
        1_700_000_000,
        100,
    )
    .with_max_pages(Some(1));

    let cache_list =
        build_nns_proposal_cache_list_report(&cache_list_request).expect("cache list report");
    let cache_status =
        build_nns_proposal_cache_status_report(&cache_status_request).expect("cache status report");
    let proposal_list = build_nns_proposal_list_report_from_cache(&list_request, &root)
        .expect("cached list lookup")
        .expect("cached list report");
    let cached_detail = build_nns_proposal_report_from_cache(&detail_request, &root)
        .expect("cached detail lookup")
        .expect("cached detail report");
    let refresh_report = sample_nns_proposal_refresh_report(&root);

    assert_eq!(cache_list_request.icp_root(), root.as_path());
    assert_eq!(cache_status_request.icp_root(), root.as_path());
    assert_eq!(cache_list.cache_count, 1);
    assert!(cache_status.found);
    assert_eq!(proposal_list.data_source, "cache");
    assert_eq!(proposal_list.proposal_count, 1);
    assert_eq!(cached_detail.data_source, "cache");
    assert_eq!(cached_detail.proposal_id, 132_411);
    assert!(nns_proposal_cache_path(&root, "ic").is_file());
    assert!(nns_proposal_cache_root(&root, "ic").ends_with("proposals"));
    assert!(nns_proposal_refresh_lock_path(&root, "ic").ends_with("full.refresh.lock"));
    assert!(nns_proposal_refresh_attempt_path(&root, "ic").ends_with("full.refresh-attempt.json"));
    assert_eq!(DEFAULT_NNS_PROPOSAL_REFRESH_LOCK_STALE_SECONDS, 1_800);
    assert!(nns_proposal_cache_list_report_text(&cache_list).contains("cache_count: 1"));
    assert!(nns_proposal_cache_status_report_text(&cache_status).contains("found: yes"));
    assert!(nns_proposal_refresh_report_text(&refresh_report).contains("proposal_count: 1"));
    assert!(request_report_api_accepts_public_types(
        build_nns_proposal_list_report,
        &list_request
    ));
    assert!(request_report_api_accepts_public_types(
        build_nns_proposal_report,
        &detail_request
    ));
    assert!(request_report_api_accepts_public_types(
        refresh_nns_proposal_cache,
        &refresh_request
    ));

    let _ = fs::remove_dir_all(root);
}

#[cfg(feature = "host")]
#[test]
fn public_nns_proposal_host_api_accepts_custom_source_adapter() {
    let root = temp_root("nns-proposal-source-public-api");
    let source = FixtureNnsProposalSource;
    let list_request = NnsProposalListRequest::new(
        "ic",
        DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT,
        1_700_000_000,
        25,
    )
    .with_status(NnsProposalStatusFilter::Executed)
    .with_reward_status(NnsProposalRewardStatusFilter::Settled)
    .with_topic(NnsProposalTopicFilter::Governance);
    let detail_request = NnsProposalRequest::new(
        "ic",
        DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT,
        1_700_000_000,
        132_411,
    );
    let refresh_request = NnsProposalRefreshRequest::new(
        &root,
        "ic",
        DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT,
        1_700_000_000,
        2,
    );

    let list = build_nns_proposal_list_report_with_source(&list_request, &source)
        .expect("proposal list report");
    let detail = build_nns_proposal_report_with_source(&detail_request, &source)
        .expect("proposal detail report");
    let refresh = refresh_nns_proposal_cache_with_source(&refresh_request, &source)
        .expect("proposal refresh report");

    assert_eq!(list.proposal_count, 1);
    assert_eq!(list.data_source, "live");
    assert_eq!(detail.proposal_id, 132_411);
    assert_eq!(detail.proposal.title.as_deref(), Some("Upgrade subnet"));
    assert_eq!(refresh.proposal_count, 1);
    assert!(refresh.complete);
    assert!(nns_proposal_cache_path(&root, "ic").is_file());

    let _ = fs::remove_dir_all(root);
}

#[cfg(feature = "host")]
struct FixtureNnsProposalSource;

#[cfg(feature = "host")]
impl NnsProposalSource for FixtureNnsProposalSource {
    fn fetch_proposals(
        &self,
        request: &NnsProposalSourceRequest,
        limit: u32,
        before_proposal_id: Option<u64>,
        status: NnsProposalStatusFilter,
        reward_status: NnsProposalRewardStatusFilter,
    ) -> Result<Vec<NnsProposalRow>, NnsProposalHostError> {
        assert_proposal_source_request(request);
        match (limit, before_proposal_id, status, reward_status) {
            (
                25,
                None,
                NnsProposalStatusFilter::Executed,
                NnsProposalRewardStatusFilter::Settled,
            )
            | (2, None, NnsProposalStatusFilter::Any, NnsProposalRewardStatusFilter::Any) => {
                Ok(vec![sample_nns_proposal_row()])
            }
            other => panic!("unexpected proposal source call: {other:?}"),
        }
    }

    fn fetch_proposal(
        &self,
        request: &NnsProposalSourceRequest,
        proposal_id: u64,
    ) -> Result<NnsProposalRow, NnsProposalHostError> {
        assert_proposal_source_request(request);
        assert_eq!(proposal_id, 132_411);
        Ok(sample_nns_proposal_row())
    }
}

#[cfg(feature = "host")]
fn assert_proposal_source_request(request: &NnsProposalSourceRequest) {
    assert_eq!(request.network, "ic");
    assert_eq!(request.endpoint, DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT);
    assert!(!request.fetched_at.is_empty());
    assert_eq!(request.fetched_by, "ic-query");
}

#[cfg(feature = "host")]
fn write_nns_inventory_fixture_caches(root: &Path) {
    write_json_cache(
        &nns_node_cache_path(root, "ic"),
        &sample_nns_node_list_report(),
    );
    write_json_cache(
        &nns_data_center_cache_path(root, "ic"),
        &sample_nns_data_center_list_report(),
    );
    write_json_cache(
        &nns_node_provider_cache_path(root, "ic"),
        &sample_nns_node_provider_list_report(),
    );
    write_json_cache(
        &nns_node_operator_cache_path(root, "ic"),
        &sample_nns_node_operator_list_report(),
    );
}

#[cfg(feature = "host")]
fn write_nns_proposal_fixture_cache(root: &Path) {
    write_json_cache(
        &nns_proposal_cache_path(root, "ic"),
        &serde_json::json!({
            "schema_version": 1,
            "network": "ic",
            "source_endpoint": DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT,
            "fetched_at": "2023-11-14T22:13:20Z",
            "fetched_by": "ic-query",
            "domain": "nns",
            "entity": "governance",
            "collection": "proposals",
            "scope": "full",
            "governance_canister_id": "rrkah-fqaaa-aaaaa-aaaaq-cai",
            "completeness": {
                "status": "api_exhausted",
                "page_size": 100,
                "page_count": 1,
                "row_count": 1,
                "point_in_time_guaranteed": false
            },
            "proposals": [sample_nns_proposal_row()]
        }),
    );
}

#[cfg(feature = "host")]
fn assert_public_nns_node_host_api(root: &Path) {
    let cache = NnsNodeCacheRequest::new(root, "ic");
    let request = NnsNodeListRequest::new(
        cache.clone(),
        DEFAULT_NNS_NODE_SOURCE_ENDPOINT,
        1_700_000_000,
    );
    let list = build_nns_node_list_report(&request).expect("build node list from cache");
    let info = build_nns_node_info_report(&NnsNodeInfoRequest::new(
        cache.clone(),
        DEFAULT_NNS_NODE_SOURCE_ENDPOINT,
        sample_nns_node_row().node_principal,
        1_700_000_000,
    ))
    .expect("build node info from cache");
    let refresh = sample_nns_node_refresh_report(root);
    let refresh_request = NnsNodeRefreshRequest::new(
        cache,
        DEFAULT_NNS_NODE_SOURCE_ENDPOINT,
        1_700_000_000,
        DEFAULT_NODE_REFRESH_LOCK_STALE_SECONDS,
    )
    .with_dry_run(true);

    assert_eq!(list.node_count, 1);
    assert_eq!(info.node_principal, sample_nns_node_row().node_principal);
    assert!(nns_node_cache_path(root, "ic").is_file());
    assert!(nns_node_refresh_lock_path(root, "ic").ends_with("refresh.lock"));
    assert!(nns_node_refresh_report_text(&refresh).contains("node_count: 1"));
    assert!(refresh_api_accepts_public_types(
        refresh_nns_node_report,
        &refresh_request
    ));
}

#[cfg(feature = "host")]
fn assert_public_nns_data_center_host_api(root: &Path) {
    let cache = NnsDataCenterCacheRequest::new(root, "ic");
    let request = NnsDataCenterListRequest::new(
        cache.clone(),
        DEFAULT_NNS_DATA_CENTER_SOURCE_ENDPOINT,
        1_700_000_000,
    );
    let list =
        build_nns_data_center_list_report(&request).expect("build data-center list from cache");
    let info = build_nns_data_center_info_report(&NnsDataCenterInfoRequest::new(
        cache.clone(),
        DEFAULT_NNS_DATA_CENTER_SOURCE_ENDPOINT,
        sample_nns_data_center_row().data_center_id,
        1_700_000_000,
    ))
    .expect("build data-center info from cache");
    let refresh = sample_nns_data_center_refresh_report(root);
    let refresh_request = NnsDataCenterRefreshRequest::new(
        cache,
        DEFAULT_NNS_DATA_CENTER_SOURCE_ENDPOINT,
        1_700_000_000,
        DEFAULT_DATA_CENTER_REFRESH_LOCK_STALE_SECONDS,
    )
    .with_dry_run(true);

    assert_eq!(list.data_center_count, 1);
    assert_eq!(
        info.data_center_id,
        sample_nns_data_center_row().data_center_id
    );
    assert!(nns_data_center_cache_path(root, "ic").is_file());
    assert!(nns_data_center_refresh_lock_path(root, "ic").ends_with("refresh.lock"));
    assert!(nns_data_center_refresh_report_text(&refresh).contains("data_center_count: 1"));
    assert!(refresh_api_accepts_public_types(
        refresh_nns_data_center_report,
        &refresh_request
    ));
}

#[cfg(feature = "host")]
fn assert_public_nns_node_provider_host_api(root: &Path) {
    let cache = NnsNodeProviderCacheRequest::new(root, "ic");
    let request =
        NnsNodeProviderListRequest::new(cache.clone(), DEFAULT_NNS_SOURCE_ENDPOINT, 1_700_000_000);
    let list =
        build_nns_node_provider_list_report(&request).expect("build node-provider list from cache");
    let info = build_nns_node_provider_info_report(&NnsNodeProviderInfoRequest::new(
        cache.clone(),
        DEFAULT_NNS_SOURCE_ENDPOINT,
        sample_nns_node_provider_row().node_provider_principal,
        1_700_000_000,
    ))
    .expect("build node-provider info from cache");
    let refresh = sample_nns_node_provider_refresh_report(root);
    let refresh_request = NnsNodeProviderRefreshRequest::new(
        cache,
        DEFAULT_NNS_SOURCE_ENDPOINT,
        1_700_000_000,
        DEFAULT_NODE_PROVIDER_REFRESH_LOCK_STALE_SECONDS,
    )
    .with_dry_run(true);

    assert_eq!(list.node_provider_count, 1);
    assert_eq!(
        info.node_provider_principal,
        sample_nns_node_provider_row().node_provider_principal
    );
    assert!(nns_node_provider_cache_path(root, "ic").is_file());
    assert!(nns_node_provider_refresh_lock_path(root, "ic").ends_with("refresh.lock"));
    assert!(nns_node_provider_refresh_report_text(&refresh).contains("node_provider_count: 1"));
    assert!(refresh_api_accepts_public_types(
        refresh_nns_node_provider_report,
        &refresh_request
    ));
}

#[cfg(feature = "host")]
fn assert_public_nns_node_operator_host_api(root: &Path) {
    let cache = NnsNodeOperatorCacheRequest::new(root, "ic");
    let request = NnsNodeOperatorListRequest::new(
        cache.clone(),
        DEFAULT_NNS_NODE_OPERATOR_SOURCE_ENDPOINT,
        1_700_000_000,
    );
    let list =
        build_nns_node_operator_list_report(&request).expect("build node-operator list from cache");
    let info = build_nns_node_operator_info_report(&NnsNodeOperatorInfoRequest::new(
        cache.clone(),
        DEFAULT_NNS_NODE_OPERATOR_SOURCE_ENDPOINT,
        sample_nns_node_operator_row().node_operator_principal,
        1_700_000_000,
    ))
    .expect("build node-operator info from cache");
    let refresh = sample_nns_node_operator_refresh_report(root);
    let refresh_request = NnsNodeOperatorRefreshRequest::new(
        cache,
        DEFAULT_NNS_NODE_OPERATOR_SOURCE_ENDPOINT,
        1_700_000_000,
        DEFAULT_NODE_OPERATOR_REFRESH_LOCK_STALE_SECONDS,
    )
    .with_dry_run(true);

    assert_eq!(list.node_operator_count, 1);
    assert_eq!(
        info.node_operator_principal,
        sample_nns_node_operator_row().node_operator_principal
    );
    assert!(nns_node_operator_cache_path(root, "ic").is_file());
    assert!(nns_node_operator_refresh_lock_path(root, "ic").ends_with("refresh.lock"));
    assert!(nns_node_operator_refresh_report_text(&refresh).contains("node_operator_count: 1"));
    assert!(refresh_api_accepts_public_types(
        refresh_nns_node_operator_report,
        &refresh_request
    ));
}

#[cfg(feature = "host")]
fn write_json_cache<T>(path: &Path, value: &T)
where
    T: Serialize,
{
    fs::create_dir_all(path.parent().expect("cache parent")).expect("create cache parent");
    fs::write(
        path,
        serde_json::to_string_pretty(value).expect("serialize fixture cache"),
    )
    .expect("write fixture cache");
}

#[cfg(feature = "host")]
#[must_use]
fn refresh_api_accepts_public_types<Request, Report, Error>(
    _refresh: RefreshFn<Request, Report, Error>,
    request: &Request,
) -> bool {
    std::mem::size_of_val(request) > 0
}

#[cfg(feature = "host")]
#[must_use]
fn request_report_api_accepts_public_types<Request, Report, Error>(
    _build: RequestReportFn<Request, Report, Error>,
    request: &Request,
) -> bool {
    std::mem::size_of_val(request) > 0
}

#[cfg(feature = "host")]
#[must_use]
fn temp_root(name: &str) -> PathBuf {
    let mut path = std::env::temp_dir();
    path.push(format!("ic-query-{name}-{}", std::process::id()));
    let _ = fs::remove_dir_all(&path);
    path
}

#[cfg(feature = "host")]
fn sample_nns_node_list_report() -> NnsNodeListReport {
    let node = sample_nns_node_row();
    NnsNodeListReport {
        schema_version: 1,
        network: "ic".to_string(),
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: DEFAULT_NNS_NODE_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        node_count: 1,
        nodes: vec![node],
    }
}

#[cfg(feature = "host")]
fn sample_nns_data_center_list_report() -> NnsDataCenterListReport {
    let data_center = sample_nns_data_center_row();
    NnsDataCenterListReport {
        schema_version: 1,
        network: "ic".to_string(),
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: DEFAULT_NNS_DATA_CENTER_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        data_center_count: 1,
        data_centers: vec![data_center],
    }
}

#[cfg(feature = "host")]
fn sample_nns_node_provider_list_report() -> NnsNodeProviderListReport {
    let provider = sample_nns_node_provider_row();
    NnsNodeProviderListReport {
        schema_version: 1,
        network: "ic".to_string(),
        governance_canister_id: "rrkah-fqaaa-aaaaa-aaaaq-cai".to_string(),
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: DEFAULT_NNS_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        node_provider_count: 1,
        node_providers: vec![provider],
    }
}

#[cfg(feature = "host")]
fn sample_nns_node_operator_list_report() -> NnsNodeOperatorListReport {
    let operator = sample_nns_node_operator_row();
    NnsNodeOperatorListReport {
        schema_version: 1,
        network: "ic".to_string(),
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: DEFAULT_NNS_NODE_OPERATOR_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        node_operator_count: 1,
        node_operators: vec![operator],
    }
}

#[cfg(feature = "host")]
fn sample_nns_node_refresh_report(root: &Path) -> NnsNodeRefreshReport {
    NnsNodeRefreshReport {
        schema_version: 1,
        network: "ic".to_string(),
        cache_path: nns_node_cache_path(root, "ic").display().to_string(),
        refresh_lock_path: nns_node_refresh_lock_path(root, "ic").display().to_string(),
        output_path: None,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: DEFAULT_NNS_NODE_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        dry_run: true,
        wrote_cache: false,
        replaced_existing_cache: true,
        node_count: 1,
    }
}

#[cfg(feature = "host")]
fn sample_nns_data_center_refresh_report(root: &Path) -> NnsDataCenterRefreshReport {
    NnsDataCenterRefreshReport {
        schema_version: 1,
        network: "ic".to_string(),
        cache_path: nns_data_center_cache_path(root, "ic").display().to_string(),
        refresh_lock_path: nns_data_center_refresh_lock_path(root, "ic")
            .display()
            .to_string(),
        output_path: None,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: DEFAULT_NNS_DATA_CENTER_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        dry_run: true,
        wrote_cache: false,
        replaced_existing_cache: true,
        data_center_count: 1,
    }
}

#[cfg(feature = "host")]
fn sample_nns_node_provider_refresh_report(root: &Path) -> NnsNodeProviderRefreshReport {
    NnsNodeProviderRefreshReport {
        schema_version: 1,
        network: "ic".to_string(),
        cache_path: nns_node_provider_cache_path(root, "ic")
            .display()
            .to_string(),
        refresh_lock_path: nns_node_provider_refresh_lock_path(root, "ic")
            .display()
            .to_string(),
        output_path: None,
        governance_canister_id: "rrkah-fqaaa-aaaaa-aaaaq-cai".to_string(),
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: DEFAULT_NNS_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        dry_run: true,
        wrote_cache: false,
        replaced_existing_cache: true,
        node_provider_count: 1,
    }
}

#[cfg(feature = "host")]
fn sample_nns_node_operator_refresh_report(root: &Path) -> NnsNodeOperatorRefreshReport {
    NnsNodeOperatorRefreshReport {
        schema_version: 1,
        network: "ic".to_string(),
        cache_path: nns_node_operator_cache_path(root, "ic")
            .display()
            .to_string(),
        refresh_lock_path: nns_node_operator_refresh_lock_path(root, "ic")
            .display()
            .to_string(),
        output_path: None,
        registry_canister_id: "rwlgt-iiaaa-aaaaa-aaaaa-cai".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: DEFAULT_NNS_NODE_OPERATOR_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        dry_run: true,
        wrote_cache: false,
        replaced_existing_cache: true,
        node_operator_count: 1,
    }
}

#[cfg(feature = "host")]
fn sample_nns_proposal_refresh_report(root: &Path) -> NnsProposalRefreshReport {
    NnsProposalRefreshReport {
        schema_version: 1,
        network: "ic".to_string(),
        governance_canister_id: "rrkah-fqaaa-aaaaa-aaaaq-cai".to_string(),
        proposal_count: 1,
        page_size: 100,
        page_count: 1,
        complete: true,
        replaced_existing_cache: false,
        wrote_cache: false,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: DEFAULT_NNS_PROPOSAL_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        cache_path: nns_proposal_cache_path(root, "ic").display().to_string(),
        refresh_attempt_path: nns_proposal_refresh_attempt_path(root, "ic")
            .display()
            .to_string(),
        refresh_lock_path: nns_proposal_refresh_lock_path(root, "ic")
            .display()
            .to_string(),
    }
}

fn sample_nns_node_row() -> NnsNodeRow {
    NnsNodeRow {
        node_principal: "zh3jp-xqaaa-aaaar-qaada-cai".to_string(),
        node_operator_principal: "qoctq-giaaa-aaaar-qaada-cai".to_string(),
        node_provider_principal: "w6gnz-6qaaa-aaaar-qaada-cai".to_string(),
        subnet_principal: "tdb26-jop6g-7sc54-foywl".to_string(),
        subnet_kind: NNS_NODE_SUBNET_KIND_APPLICATION.to_string(),
        data_center_id: "zh1".to_string(),
    }
}

fn sample_nns_data_center_row() -> NnsDataCenterRow {
    NnsDataCenterRow {
        data_center_id: "zh1".to_string(),
        region: "Zurich".to_string(),
        owner: "Example DC Owner".to_string(),
        latitude: Some(47.37),
        longitude: Some(8.54),
        node_operator_count: 2,
        node_provider_count: 3,
        node_count: 12,
    }
}

fn sample_nns_node_provider_row() -> NnsNodeProviderRow {
    NnsNodeProviderRow {
        node_provider_principal: "w6gnz-6qaaa-aaaar-qaada-cai".to_string(),
        name: Some("Example Provider".to_string()),
        node_count: Some(12),
        reward_account_hex: Some("deadbeef".to_string()),
    }
}

fn sample_nns_node_operator_row() -> NnsNodeOperatorRow {
    NnsNodeOperatorRow {
        node_operator_principal: "tdb26-jop6g-7sc54-foywl".to_string(),
        node_provider_principal: "w6gnz-6qaaa-aaaar-qaada-cai".to_string(),
        node_allowance: 28,
        data_center_id: "zh1".to_string(),
        node_count: Some(12),
    }
}

fn sample_nns_proposal_row() -> NnsProposalRow {
    NnsProposalRow {
        proposal_id: Some(132_411),
        proposer_neuron_id: Some(12_345),
        topic: 4,
        topic_text: "governance".to_string(),
        status: 4,
        status_text: "executed".to_string(),
        reward_status: 3,
        reward_status_text: "settled".to_string(),
        title: Some("Upgrade subnet".to_string()),
        summary: "Upgrade subnet replica version.".to_string(),
        url: "https://dashboard.internetcomputer.org/proposal/132411".to_string(),
        action_text: Some("execute-nns-function".to_string()),
        reject_cost_e8s: 100_000_000,
        proposal_timestamp_seconds: 1_700_000_000,
        proposed_at: "2023-11-14T22:13:20Z".to_string(),
        deadline_timestamp_seconds: Some(1_700_086_400),
        deadline_at: Some("2023-11-15T22:13:20Z".to_string()),
        decided_timestamp_seconds: 1_700_010_000,
        decided_at: Some("2023-11-15T01:00:00Z".to_string()),
        executed_timestamp_seconds: 1_700_020_000,
        executed_at: Some("2023-11-15T03:46:40Z".to_string()),
        failed_timestamp_seconds: 0,
        failed_at: None,
        reward_event_round: 42,
        total_potential_voting_power: Some(1_000_000_000),
        latest_tally: Some(NnsProposalTally {
            timestamp_seconds: 1_700_010_000,
            yes: 900_000_000,
            no: 100_000_000,
            total: 1_000_000_000,
        }),
        ballot_count: 1,
        ballots: vec![NnsProposalBallotRow {
            neuron_id: 12_345,
            vote: 1,
            vote_text: "yes".to_string(),
            voting_power: 100_000_000,
        }],
    }
}

fn sample_topology_registry_version_row() -> NnsTopologyRegistryVersionRow {
    NnsTopologyRegistryVersionRow {
        source: "subnet_catalog".to_string(),
        registry_version: 42,
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        stale: Some(false),
    }
}
