#[cfg(feature = "host")]
use ic_query::sns::{
    DEFAULT_SNS_NEURONS_REFRESH_LOCK_STALE_SECONDS,
    DEFAULT_SNS_PROPOSALS_REFRESH_LOCK_STALE_SECONDS, DEFAULT_SNS_SOURCE_ENDPOINT, LiveSnsSource,
    MainnetSns, MainnetSnsList, MainnetSnsProposal, MainnetSnsProposalPage, MainnetSnsProposals,
    MainnetSnsToken, SnsHostError, SnsListSource, SnsNeuronRow, SnsNeuronsCacheListRequest,
    SnsNeuronsCacheStatusRequest, SnsNeuronsRefreshReport, SnsNeuronsRefreshRequest,
    SnsNeuronsReport, SnsNeuronsRequest, SnsNeuronsSort, SnsParamsSource, SnsProposalSource,
    SnsProposalsCacheListRequest, SnsProposalsCacheStatusRequest, SnsProposalsRefreshReport,
    SnsProposalsRefreshRequest, SnsProposalsSource, SnsSourceRequest, SnsTokenSource,
    build_sns_info_report, build_sns_info_report_with_source, build_sns_list_report,
    build_sns_list_report_with_source, build_sns_neurons_cache_list_report,
    build_sns_neurons_cache_status_report, build_sns_neurons_report, build_sns_params_report,
    build_sns_params_report_with_source, build_sns_proposal_report,
    build_sns_proposal_report_with_source, build_sns_proposals_cache_list_report,
    build_sns_proposals_cache_status_report, build_sns_proposals_report,
    build_sns_proposals_report_with_source, build_sns_token_report,
    build_sns_token_report_with_source, refresh_sns_neurons_cache, refresh_sns_proposals_cache,
    refresh_sns_proposals_cache_with_source, sns_neurons_cache_list_report_text,
    sns_neurons_cache_path, sns_neurons_cache_status_report_text, sns_neurons_refresh_attempt_path,
    sns_neurons_refresh_lock_path, sns_neurons_refresh_report_text, sns_neurons_report_text,
    sns_proposals_cache_list_report_text, sns_proposals_cache_path,
    sns_proposals_cache_status_report_text, sns_proposals_refresh_attempt_path,
    sns_proposals_refresh_lock_path, sns_proposals_refresh_report_text,
};
use ic_query::sns::{
    SnsCustomProposalCriticality, SnsGovernanceParameters, SnsInfoReport, SnsInfoRequest,
    SnsListReport, SnsListRequest, SnsListSort, SnsLookupRequest, SnsNeuronPermissionList,
    SnsParamsReport, SnsParamsRequest, SnsProposalBallotRow, SnsProposalEligibilityFilter,
    SnsProposalFailureReason, SnsProposalReport, SnsProposalRequest, SnsProposalRow,
    SnsProposalSortDirection, SnsProposalStatusFilter, SnsProposalTally, SnsProposalTopicFilter,
    SnsProposalsReport, SnsProposalsRequest, SnsProposalsSort, SnsTokenMetadataRow, SnsTokenReport,
    SnsTokenRequest, SnsTokenStandardRow, SnsVotingRewardsParameters, sns_info_report_text,
    sns_list_report_text, sns_params_report_text, sns_proposal_report_text,
    sns_proposals_report_text, sns_token_report_text,
};
use serde_json::json;
#[cfg(feature = "host")]
use std::{
    fs,
    path::{Path, PathBuf},
};

#[cfg(feature = "host")]
const SAMPLE_SNS_ROOT_CANISTER_ID: &str = "be2us-64aaa-aaaaa-qaabq-cai";
#[cfg(feature = "host")]
const SAMPLE_SNS_GOVERNANCE_CANISTER_ID: &str = "csyra-haaaa-aaaaa-qaaeq-cai";
#[cfg(feature = "host")]
const SAMPLE_SNS_LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai";
#[cfg(feature = "host")]
const SAMPLE_SNS_SWAP_CANISTER_ID: &str = "ca6gz-lqaaa-aaaaa-qaacu-cai";
#[cfg(feature = "host")]
const SAMPLE_SNS_INDEX_CANISTER_ID: &str = "qhbym-qaaaa-aaaaa-aaafq-cai";
#[cfg(feature = "host")]
const SAMPLE_SNS_FETCHED_AT: &str = "2023-11-14T22:13:20Z";

#[cfg(feature = "host")]
type SnsListBuilder = fn(&SnsListRequest) -> Result<SnsListReport, SnsHostError>;
#[cfg(feature = "host")]
type SnsInfoBuilder = fn(&SnsInfoRequest) -> Result<SnsInfoReport, SnsHostError>;
#[cfg(feature = "host")]
type SnsTokenBuilder = fn(&SnsTokenRequest) -> Result<SnsTokenReport, SnsHostError>;
#[cfg(feature = "host")]
type SnsParamsBuilder = fn(&SnsParamsRequest) -> Result<SnsParamsReport, SnsHostError>;
#[cfg(feature = "host")]
type SnsProposalsBuilder = fn(&SnsProposalsRequest) -> Result<SnsProposalsReport, SnsHostError>;
#[cfg(feature = "host")]
type SnsProposalBuilder = fn(&SnsProposalRequest) -> Result<SnsProposalReport, SnsHostError>;
#[cfg(feature = "host")]
type SnsNeuronsBuilder = fn(&SnsNeuronsRequest) -> Result<SnsNeuronsReport, SnsHostError>;
#[cfg(feature = "host")]
type SnsNeuronsRefreshBuilder =
    fn(&SnsNeuronsRefreshRequest) -> Result<SnsNeuronsRefreshReport, SnsHostError>;
#[cfg(feature = "host")]
type SnsProposalsRefreshBuilder =
    fn(&SnsProposalsRefreshRequest) -> Result<SnsProposalsRefreshReport, SnsHostError>;

#[test]
fn public_sns_request_constructors_set_expected_fields() {
    let list = SnsListRequest::new("ic", "https://icp-api.io", 1_700_000_000)
        .with_verbose(true)
        .with_sort(SnsListSort::Name);
    assert_eq!(list.network, "ic");
    assert_eq!(list.sort, SnsListSort::Name);
    assert!(list.verbose);

    let lookup = SnsLookupRequest::new("ic", "https://icp-api.io", 1_700_000_000, "1");
    assert_eq!(lookup.input, "1");
    let token = SnsTokenRequest::new(
        "ic",
        "https://icp-api.io",
        1_700_000_000,
        "be2us-64aaa-aaaaa-qaabq-cai",
    );
    assert_eq!(token.source_endpoint, "https://icp-api.io");

    let proposal = SnsProposalRequest::new("ic", "https://icp-api.io", 1_700_000_000, "1", 42)
        .with_verbose(true)
        .with_show_ballots(true);
    assert_eq!(proposal.proposal_id, 42);
    assert!(proposal.verbose);
    assert!(proposal.show_ballots);
    assert!(proposal.icp_root.is_none());

    let proposals = SnsProposalsRequest::new("ic", "https://icp-api.io", 1_700_000_000, "1", 25)
        .with_before_proposal_id(100)
        .with_status(SnsProposalStatusFilter::Open)
        .with_topic(SnsProposalTopicFilter::Governance)
        .with_eligibility(SnsProposalEligibilityFilter::Yes)
        .with_proposer_neuron_id("010203")
        .with_query("upgrade")
        .with_sort(SnsProposalsSort::Created)
        .with_sort_direction(SnsProposalSortDirection::Asc)
        .with_verbose(true);
    assert_eq!(proposals.limit, 25);
    assert_eq!(proposals.before_proposal_id, Some(100));
    assert_eq!(proposals.status, SnsProposalStatusFilter::Open);
    assert_eq!(proposals.topic, SnsProposalTopicFilter::Governance);
    assert_eq!(proposals.eligibility, SnsProposalEligibilityFilter::Yes);
    assert_eq!(proposals.proposer_neuron_id.as_deref(), Some("010203"));
    assert_eq!(proposals.query.as_deref(), Some("upgrade"));
    assert_eq!(proposals.sort, SnsProposalsSort::Created);
    assert_eq!(proposals.sort_direction, SnsProposalSortDirection::Asc);
    assert!(proposals.verbose);
}

#[test]
fn public_sns_list_api_is_constructible_and_renderable() {
    let request = SnsListRequest {
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        now_unix_secs: 1_700_000_000,
        verbose: false,
        sort: SnsListSort::Id,
    };

    assert_eq!(request.sort.as_str(), "id");

    let report = SnsListReport {
        schema_version: 3,
        network: request.network,
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        verbose: request.verbose,
        sort: request.sort.as_str().to_string(),
        sns_count: 0,
        metadata_error_count: 0,
        sns_instances: Vec::new(),
    };

    let text = sns_list_report_text(&report);

    assert!(text.contains("network: ic"));
    assert!(text.contains("sns_count: 0"));
}

#[test]
fn public_sns_info_api_is_constructible_and_renderable() {
    let request = SnsInfoRequest {
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        now_unix_secs: 1_700_000_000,
        input: "1".to_string(),
    };

    let report = SnsInfoReport {
        schema_version: 2,
        network: request.network,
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        id: 1,
        name: "Example SNS".to_string(),
        description: Some("Example description".to_string()),
        url: None,
        root_canister_id: "be2us-64aaa-aaaaa-qaabq-cai".to_string(),
        governance_canister_id: "csyra-haaaa-aaaaa-qaaeq-cai".to_string(),
        ledger_canister_id: "ryjl3-tyaaa-aaaaa-aaaba-cai".to_string(),
        swap_canister_id: "ca6gz-lqaaa-aaaaa-qaacu-cai".to_string(),
        index_canister_id: "qhbym-qaaaa-aaaaa-aaafq-cai".to_string(),
        metadata_error: None,
    };

    assert_eq!(request.input, "1");

    let text = sns_info_report_text(&report);

    assert!(text.contains("sns_id: 1"));
    assert!(text.contains("description: Example description"));
    assert!(text.contains("url: -"));
}

#[test]
fn public_sns_token_api_is_constructible_and_renderable() {
    let request = SnsTokenRequest {
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        now_unix_secs: 1_700_000_000,
        input: "be2us-64aaa-aaaaa-qaabq-cai".to_string(),
    };

    let report = SnsTokenReport {
        schema_version: 1,
        network: request.network,
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        id: 1,
        name: "Example SNS".to_string(),
        root_canister_id: request.input,
        ledger_canister_id: "ryjl3-tyaaa-aaaaa-aaaba-cai".to_string(),
        sns_index_canister_id: "qhbym-qaaaa-aaaaa-aaafq-cai".to_string(),
        token_name: "Example Token".to_string(),
        token_symbol: "EXT".to_string(),
        decimals: 8,
        transfer_fee: "100_000_000".to_string(),
        total_supply: "1_000_000_000".to_string(),
        minting_account_owner: Some("aaaaa-aa".to_string()),
        minting_account_subaccount_hex: None,
        ledger_index_canister_id: None,
        ledger_index_error: Some("not configured".to_string()),
        supported_standards: vec![SnsTokenStandardRow {
            name: "ICRC-1".to_string(),
            url: "https://github.com/dfinity/ICRC-1".to_string(),
        }],
        metadata: vec![SnsTokenMetadataRow {
            key: "icrc1:symbol".to_string(),
            value_type: "Text".to_string(),
            value: json!("EXT"),
        }],
    };

    let text = sns_token_report_text(&report);

    assert!(text.contains("token_symbol: EXT"));
    assert!(text.contains("transfer_fee: 1.00"));
    assert!(text.contains("ledger_index_error: not configured"));
    assert!(text.contains("ICRC-1"));
}

#[test]
fn public_sns_params_api_is_constructible_and_renderable() {
    let request = SnsParamsRequest {
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        now_unix_secs: 1_700_000_000,
        input: "1".to_string(),
    };

    let report = SnsParamsReport {
        schema_version: 1,
        network: request.network,
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        id: 1,
        name: "Example SNS".to_string(),
        root_canister_id: "be2us-64aaa-aaaaa-qaabq-cai".to_string(),
        governance_canister_id: "csyra-haaaa-aaaaa-qaaeq-cai".to_string(),
        parameters: sample_sns_governance_parameters(),
    };

    let text = sns_params_report_text(&report);

    assert!(text.contains("sns_id: 1"));
    assert!(text.contains("neuron_minimum_stake"));
    assert!(text.contains("1.00"));
    assert!(text.contains("max_dissolve_delay"));
    assert!(text.contains("2h"));
    assert!(text.contains("maturity_modulation_disabled"));
    assert!(text.contains("no"));
    assert!(text.contains("additional_critical_native_actions"));
    assert!(text.contains("7,8"));
}

#[test]
fn public_sns_proposals_api_is_constructible_and_renderable() {
    let request = SnsProposalsRequest {
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        now_unix_secs: 1_700_000_000,
        input: "1".to_string(),
        limit: 10,
        before_proposal_id: Some(100),
        status: SnsProposalStatusFilter::Any,
        topic: SnsProposalTopicFilter::Governance,
        eligibility: SnsProposalEligibilityFilter::Yes,
        proposer_neuron_id: Some("010203".to_string()),
        query: Some("upgrade".to_string()),
        sort: SnsProposalsSort::Created,
        sort_direction: SnsProposalSortDirection::Desc,
        icp_root: None,
        verbose: true,
    };

    assert_eq!(request.sort.as_str(), "created");
    assert_eq!(request.topic.as_str(), "governance");

    let report = SnsProposalsReport {
        schema_version: 10,
        network: request.network,
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        id: 1,
        name: "Example SNS".to_string(),
        root_canister_id: "be2us-64aaa-aaaaa-qaabq-cai".to_string(),
        governance_canister_id: "csyra-haaaa-aaaaa-qaaeq-cai".to_string(),
        requested_limit: request.limit,
        before_proposal_id: request.before_proposal_id,
        status_filter: request.status.as_str().to_string(),
        topic_filter: request.topic.as_str().to_string(),
        eligibility_filter: request.eligibility.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(),
        verbose: request.verbose,
        data_source: "api".to_string(),
        cache_path: None,
        cache_complete: None,
        proposal_count: 1,
        proposals: vec![sample_sns_proposal_row()],
    };

    let text = sns_proposals_report_text(&report);

    assert!(text.contains("proposal_count: 1"));
    assert!(text.contains("topic_filter: governance"));
    assert!(text.contains("proposal_details:"));
    assert!(text.contains("title: Upgrade SNS"));
}

#[test]
fn public_sns_proposal_api_is_constructible_and_renderable() {
    let request = SnsProposalRequest {
        network: "ic".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        now_unix_secs: 1_700_000_000,
        input: "1".to_string(),
        proposal_id: 42,
        icp_root: None,
        verbose: false,
        show_ballots: true,
    };

    let report = SnsProposalReport {
        schema_version: 5,
        network: request.network,
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: "2023-11-14T22:13:20Z".to_string(),
        source_endpoint: request.source_endpoint,
        fetched_by: "ic-query".to_string(),
        id: 1,
        name: "Example SNS".to_string(),
        root_canister_id: "be2us-64aaa-aaaaa-qaabq-cai".to_string(),
        governance_canister_id: "csyra-haaaa-aaaaa-qaaeq-cai".to_string(),
        proposal_id: request.proposal_id,
        verbose: request.verbose,
        show_ballots: request.show_ballots,
        data_source: "api".to_string(),
        cache_path: None,
        cache_complete: None,
        proposal: sample_sns_proposal_row(),
    };

    let text = sns_proposal_report_text(&report);

    assert!(text.contains("proposal_id: 42"));
    assert!(text.contains("show_ballots: yes"));
    assert!(text.contains("ballots:"));
    assert!(text.contains("Upgrade SNS"));
}

#[cfg(feature = "host")]
#[test]
fn public_sns_host_api_exposes_live_builder_entry_points() {
    accepts_public_function::<SnsListBuilder>(build_sns_list_report);
    accepts_public_function::<SnsInfoBuilder>(build_sns_info_report);
    accepts_public_function::<SnsTokenBuilder>(build_sns_token_report);
    accepts_public_function::<SnsParamsBuilder>(build_sns_params_report);
    accepts_public_function::<SnsProposalsBuilder>(build_sns_proposals_report);
    accepts_public_function::<SnsProposalBuilder>(build_sns_proposal_report);
    accepts_public_function::<SnsNeuronsBuilder>(build_sns_neurons_report);
    accepts_public_function::<SnsNeuronsRefreshBuilder>(refresh_sns_neurons_cache);
    accepts_public_function::<SnsProposalsRefreshBuilder>(refresh_sns_proposals_cache);
    let live_source = LiveSnsSource;
    accepts_public_function(live_source);
    assert_eq!(DEFAULT_SNS_NEURONS_REFRESH_LOCK_STALE_SECONDS, 30 * 60);
    assert_eq!(DEFAULT_SNS_PROPOSALS_REFRESH_LOCK_STALE_SECONDS, 30 * 60);
}

#[cfg(feature = "host")]
#[test]
fn public_sns_host_api_accepts_custom_source_adapters() -> Result<(), SnsHostError> {
    let source = FixtureSnsSource;
    let source_request = SnsSourceRequest::new(
        DEFAULT_SNS_SOURCE_ENDPOINT,
        SAMPLE_SNS_FETCHED_AT,
        "fixture",
    );
    assert_eq!(source_request.endpoint, DEFAULT_SNS_SOURCE_ENDPOINT);

    let list_request = SnsListRequest::new("ic", DEFAULT_SNS_SOURCE_ENDPOINT, 1_700_000_000);
    let list = build_sns_list_report_with_source(&list_request, &source)?;
    assert_eq!(list.sns_count, 1);
    assert_eq!(list.sns_instances[0].id, 1);

    let info_request = SnsInfoRequest::new("ic", DEFAULT_SNS_SOURCE_ENDPOINT, 1_700_000_000, "1");
    let info = build_sns_info_report_with_source(&info_request, &source)?;
    assert_eq!(info.root_canister_id, SAMPLE_SNS_ROOT_CANISTER_ID);

    let token_request = SnsTokenRequest::new(
        "ic",
        DEFAULT_SNS_SOURCE_ENDPOINT,
        1_700_000_000,
        SAMPLE_SNS_ROOT_CANISTER_ID,
    );
    let token = build_sns_token_report_with_source(&token_request, &source)?;
    assert_eq!(token.token_symbol, "EXT");

    let params_request =
        SnsParamsRequest::new("ic", DEFAULT_SNS_SOURCE_ENDPOINT, 1_700_000_000, "1");
    let params = build_sns_params_report_with_source(&params_request, &source)?;
    assert_eq!(
        params.parameters.neuron_minimum_stake_e8s,
        Some(100_000_000)
    );

    Ok(())
}

#[cfg(feature = "host")]
#[test]
fn public_sns_host_api_accepts_custom_proposal_source_adapters() -> Result<(), SnsHostError> {
    let source = FixtureSnsSource;
    let detail_request =
        SnsProposalRequest::new("ic", DEFAULT_SNS_SOURCE_ENDPOINT, 1_700_000_000, "1", 42)
            .with_show_ballots(true);
    let detail = build_sns_proposal_report_with_source(&detail_request, &source)?;
    assert_eq!(detail.proposal.proposal_id, Some(42));
    assert_eq!(detail.data_source, "live");

    let list_request =
        SnsProposalsRequest::new("ic", DEFAULT_SNS_SOURCE_ENDPOINT, 1_700_000_000, "1", 10)
            .with_before_proposal_id(99)
            .with_status(SnsProposalStatusFilter::Open)
            .with_topic(SnsProposalTopicFilter::Governance);
    let list = build_sns_proposals_report_with_source(&list_request, &source)?;
    assert_eq!(list.proposal_count, 1);
    assert_eq!(list.proposals[0].title, "Upgrade SNS");

    let cache_root = proposal_source_cache_root();
    let _ = fs::remove_dir_all(&cache_root);
    let refresh_request = SnsProposalsRefreshRequest::new(
        cache_root.clone(),
        "ic",
        DEFAULT_SNS_SOURCE_ENDPOINT,
        1_700_000_000,
        "1",
        100,
    )
    .with_max_pages(Some(1));
    let refresh = refresh_sns_proposals_cache_with_source(&refresh_request, &source)?;
    assert_eq!(refresh.proposal_count, 1);
    assert!(refresh.complete);
    let _ = fs::remove_dir_all(cache_root);

    Ok(())
}

#[cfg(feature = "host")]
#[test]
fn public_sns_host_api_exposes_neuron_request_constructor() {
    let cache_root = PathBuf::from("target/ic-query-sns-public-api-empty-root");
    let request = SnsNeuronsRequest::new(
        "ic",
        DEFAULT_SNS_SOURCE_ENDPOINT,
        1_700_000_000,
        SAMPLE_SNS_ROOT_CANISTER_ID,
        50,
    )
    .with_owner_principal_id("aaaaa-aa")
    .with_sort(SnsNeuronsSort::Stake)
    .with_icp_root(cache_root.clone())
    .with_verbose(true);

    assert_eq!(request.input, SAMPLE_SNS_ROOT_CANISTER_ID);
    assert_eq!(request.limit, 50);
    assert_eq!(request.owner_principal_id.as_deref(), Some("aaaaa-aa"));
    assert_eq!(request.sort, SnsNeuronsSort::Stake);
    assert_eq!(request.icp_root.as_deref(), Some(cache_root.as_path()));
    assert!(request.verbose);
}

#[cfg(feature = "host")]
#[test]
fn public_sns_host_api_exposes_cache_paths_and_local_reports() -> Result<(), SnsHostError> {
    let cache_root = PathBuf::from("target/ic-query-sns-public-api-empty-root");

    let neurons_cache_path = sns_neurons_cache_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    let neurons_lock_path =
        sns_neurons_refresh_lock_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    let neurons_attempt_path =
        sns_neurons_refresh_attempt_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    assert_eq!(
        neurons_lock_path,
        neurons_cache_path.with_file_name("full.refresh.lock")
    );
    assert_eq!(
        neurons_attempt_path,
        neurons_cache_path.with_file_name("full.refresh-attempt.json")
    );

    let proposals_cache_path =
        sns_proposals_cache_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    let proposals_lock_path =
        sns_proposals_refresh_lock_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    let proposals_attempt_path =
        sns_proposals_refresh_attempt_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    assert_eq!(
        proposals_lock_path,
        proposals_cache_path.with_file_name("full.refresh.lock")
    );
    assert_eq!(
        proposals_attempt_path,
        proposals_cache_path.with_file_name("full.refresh-attempt.json")
    );

    let neurons_list_request = SnsNeuronsCacheListRequest::new(cache_root.clone(), "ic");
    assert_eq!(neurons_list_request.icp_root(), cache_root.as_path());
    let neurons_list_report = build_sns_neurons_cache_list_report(&neurons_list_request)?;
    assert_eq!(neurons_list_report.cache_count, 0);
    assert!(sns_neurons_cache_list_report_text(&neurons_list_report).contains("cache_count: 0"));

    let neurons_status_request =
        SnsNeuronsCacheStatusRequest::new(cache_root.clone(), "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    assert_eq!(neurons_status_request.icp_root(), cache_root.as_path());
    let neurons_status_report = build_sns_neurons_cache_status_report(&neurons_status_request)?;
    assert!(!neurons_status_report.found);
    let expected_neurons_cache_path = neurons_cache_path.display().to_string();
    assert_eq!(
        neurons_status_report.expected_cache_path.as_deref(),
        Some(expected_neurons_cache_path.as_str())
    );
    assert!(sns_neurons_cache_status_report_text(&neurons_status_report).contains("found: no"));

    let proposals_list_request = SnsProposalsCacheListRequest::new(cache_root.clone(), "ic");
    assert_eq!(proposals_list_request.icp_root(), cache_root.as_path());
    let proposals_list_report = build_sns_proposals_cache_list_report(&proposals_list_request)?;
    assert_eq!(proposals_list_report.cache_count, 0);
    assert!(
        sns_proposals_cache_list_report_text(&proposals_list_report).contains("cache_count: 0")
    );

    let proposals_status_request =
        SnsProposalsCacheStatusRequest::new(cache_root.clone(), "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    assert_eq!(proposals_status_request.icp_root(), cache_root.as_path());
    let proposals_status_report =
        build_sns_proposals_cache_status_report(&proposals_status_request)?;
    assert!(!proposals_status_report.found);
    let expected_proposals_cache_path = proposals_cache_path.display().to_string();
    assert_eq!(
        proposals_status_report.expected_cache_path.as_deref(),
        Some(expected_proposals_cache_path.as_str())
    );
    assert!(sns_proposals_cache_status_report_text(&proposals_status_report).contains("found: no"));

    Ok(())
}

#[cfg(feature = "host")]
#[test]
fn public_sns_host_api_exposes_refresh_requests_and_renderers() {
    let cache_root = PathBuf::from("target/ic-query-sns-public-api-empty-root");
    let neurons_cache_path = sns_neurons_cache_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    let neurons_lock_path =
        sns_neurons_refresh_lock_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    let neurons_attempt_path =
        sns_neurons_refresh_attempt_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    let proposals_cache_path =
        sns_proposals_cache_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    let proposals_lock_path =
        sns_proposals_refresh_lock_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);
    let proposals_attempt_path =
        sns_proposals_refresh_attempt_path(&cache_root, "ic", SAMPLE_SNS_ROOT_CANISTER_ID);

    let neurons_refresh_request = SnsNeuronsRefreshRequest::new(
        cache_root.clone(),
        "ic",
        DEFAULT_SNS_SOURCE_ENDPOINT,
        1_700_000_000,
        SAMPLE_SNS_ROOT_CANISTER_ID,
        500,
    )
    .with_max_pages(Some(2));
    assert_eq!(neurons_refresh_request.max_pages, Some(2));

    let proposals_refresh_request = SnsProposalsRefreshRequest::new(
        cache_root,
        "ic",
        DEFAULT_SNS_SOURCE_ENDPOINT,
        1_700_000_000,
        SAMPLE_SNS_ROOT_CANISTER_ID,
        100,
    )
    .with_max_pages(Some(3));
    assert_eq!(proposals_refresh_request.max_pages, Some(3));

    assert!(sns_neurons_report_text(&sample_sns_neurons_report()).contains("neuron_count: 1"));
    assert!(
        sns_neurons_refresh_report_text(&sample_sns_neurons_refresh_report(
            &neurons_cache_path,
            &neurons_lock_path,
            &neurons_attempt_path,
        ))
        .contains("wrote_cache: yes")
    );
    assert!(
        sns_proposals_refresh_report_text(&sample_sns_proposals_refresh_report(
            &proposals_cache_path,
            &proposals_lock_path,
            &proposals_attempt_path,
        ))
        .contains("proposal_count: 1")
    );
}

#[cfg(feature = "host")]
fn accepts_public_function<T>(_function: T) {}

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

#[cfg(feature = "host")]
impl SnsListSource for FixtureSnsSource {
    fn fetch_deployed_snses(
        &self,
        request: &SnsSourceRequest,
    ) -> Result<MainnetSnsList, SnsHostError> {
        assert_eq!(request.endpoint, DEFAULT_SNS_SOURCE_ENDPOINT);
        Ok(sample_mainnet_sns_list(request))
    }
}

#[cfg(feature = "host")]
impl SnsTokenSource for FixtureSnsSource {
    fn fetch_sns_token(
        &self,
        _request: &SnsSourceRequest,
        sns: &MainnetSns,
    ) -> Result<MainnetSnsToken, SnsHostError> {
        assert_eq!(sns.root_canister_id, SAMPLE_SNS_ROOT_CANISTER_ID);
        Ok(sample_mainnet_sns_token())
    }
}

#[cfg(feature = "host")]
impl SnsParamsSource for FixtureSnsSource {
    fn fetch_sns_params(
        &self,
        _request: &SnsSourceRequest,
        sns: &MainnetSns,
    ) -> Result<SnsGovernanceParameters, SnsHostError> {
        assert_eq!(
            sns.governance_canister_id,
            SAMPLE_SNS_GOVERNANCE_CANISTER_ID
        );
        Ok(sample_sns_governance_parameters())
    }
}

#[cfg(feature = "host")]
impl SnsProposalSource for FixtureSnsSource {
    fn fetch_sns_proposal(
        &self,
        _request: &SnsSourceRequest,
        sns: &MainnetSns,
        proposal_id: u64,
    ) -> Result<MainnetSnsProposal, SnsHostError> {
        assert_eq!(
            sns.governance_canister_id,
            SAMPLE_SNS_GOVERNANCE_CANISTER_ID
        );
        assert_eq!(proposal_id, 42);
        Ok(MainnetSnsProposal {
            proposal: sample_sns_proposal_row(),
        })
    }
}

#[cfg(feature = "host")]
impl SnsProposalsSource for FixtureSnsSource {
    fn fetch_sns_proposals(
        &self,
        _request: &SnsSourceRequest,
        sns: &MainnetSns,
        limit: u32,
        before_proposal_id: Option<u64>,
        include_status: &[i32],
        topic: SnsProposalTopicFilter,
    ) -> Result<MainnetSnsProposals, SnsHostError> {
        assert_eq!(
            sns.governance_canister_id,
            SAMPLE_SNS_GOVERNANCE_CANISTER_ID
        );
        assert_eq!(limit, 10);
        assert_eq!(before_proposal_id, Some(99));
        assert_eq!(include_status, &[1]);
        assert_eq!(topic, SnsProposalTopicFilter::Governance);
        Ok(MainnetSnsProposals {
            proposals: vec![sample_sns_proposal_row()],
        })
    }

    fn fetch_sns_proposal_page(
        &self,
        _request: &SnsSourceRequest,
        sns: &MainnetSns,
        limit: u32,
        before_proposal_id: Option<u64>,
    ) -> Result<MainnetSnsProposalPage, SnsHostError> {
        assert_eq!(
            sns.governance_canister_id,
            SAMPLE_SNS_GOVERNANCE_CANISTER_ID
        );
        assert_eq!(limit, 100);
        assert_eq!(before_proposal_id, None);
        Ok(MainnetSnsProposalPage {
            proposals: vec![sample_sns_proposal_row()],
            last_cursor: Some(42),
        })
    }
}

#[cfg(feature = "host")]
fn proposal_source_cache_root() -> PathBuf {
    PathBuf::from(format!(
        "target/ic-query-sns-public-api-proposal-source-{}",
        std::process::id()
    ))
}

#[cfg(feature = "host")]
fn sample_mainnet_sns_list(request: &SnsSourceRequest) -> MainnetSnsList {
    MainnetSnsList {
        network: "ic".to_string(),
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: request.fetched_at.clone(),
        fetched_by: request.fetched_by.clone(),
        source_endpoint: request.endpoint.clone(),
        sns_instances: vec![sample_mainnet_sns()],
    }
}

#[cfg(feature = "host")]
fn sample_mainnet_sns() -> MainnetSns {
    MainnetSns {
        id: 0,
        name: "Example SNS".to_string(),
        description: Some("Example description".to_string()),
        url: Some("https://example.com/sns".to_string()),
        root_canister_id: SAMPLE_SNS_ROOT_CANISTER_ID.to_string(),
        governance_canister_id: SAMPLE_SNS_GOVERNANCE_CANISTER_ID.to_string(),
        ledger_canister_id: SAMPLE_SNS_LEDGER_CANISTER_ID.to_string(),
        swap_canister_id: SAMPLE_SNS_SWAP_CANISTER_ID.to_string(),
        index_canister_id: SAMPLE_SNS_INDEX_CANISTER_ID.to_string(),
        metadata_error: None,
    }
}

#[cfg(feature = "host")]
fn sample_mainnet_sns_token() -> MainnetSnsToken {
    MainnetSnsToken {
        token_name: "Example Token".to_string(),
        token_symbol: "EXT".to_string(),
        decimals: 8,
        transfer_fee: "100000000".to_string(),
        total_supply: "1000000000".to_string(),
        minting_account_owner: Some("aaaaa-aa".to_string()),
        minting_account_subaccount_hex: None,
        ledger_index_canister_id: Some(SAMPLE_SNS_INDEX_CANISTER_ID.to_string()),
        ledger_index_error: None,
        supported_standards: vec![SnsTokenStandardRow {
            name: "ICRC-1".to_string(),
            url: "https://github.com/dfinity/ICRC-1".to_string(),
        }],
        metadata: vec![SnsTokenMetadataRow {
            key: "icrc1:symbol".to_string(),
            value_type: "Text".to_string(),
            value: json!("EXT"),
        }],
    }
}

fn sample_sns_governance_parameters() -> SnsGovernanceParameters {
    SnsGovernanceParameters {
        max_dissolve_delay_seconds: Some(7_200),
        max_dissolve_delay_bonus_percentage: Some(50),
        max_followees_per_function: Some(15),
        neuron_claimer_permissions: Some(SnsNeuronPermissionList {
            permissions: vec![1, 2],
        }),
        neuron_minimum_stake_e8s: Some(100_000_000),
        max_neuron_age_for_age_bonus: Some(86_400),
        initial_voting_period_seconds: Some(3_600),
        neuron_minimum_dissolve_delay_to_vote_seconds: Some(600),
        reject_cost_e8s: Some(10_000_000),
        max_proposals_to_keep_per_action: Some(100),
        wait_for_quiet_deadline_increase_seconds: Some(300),
        max_number_of_neurons: Some(10_000),
        transaction_fee_e8s: Some(10_000),
        max_number_of_proposals_with_ballots: Some(500),
        max_age_bonus_percentage: Some(25),
        neuron_grantable_permissions: None,
        voting_rewards_parameters: Some(SnsVotingRewardsParameters {
            final_reward_rate_basis_points: Some(125),
            initial_reward_rate_basis_points: Some(250),
            reward_rate_transition_duration_seconds: Some(31_536_000),
            round_duration_seconds: Some(86_400),
        }),
        maturity_modulation_disabled: Some(false),
        max_number_of_principals_per_neuron: Some(5),
        automatically_advance_target_version: Some(true),
        custom_proposal_criticality: Some(SnsCustomProposalCriticality {
            additional_critical_native_action_ids: vec![7, 8],
        }),
    }
}

#[cfg(feature = "host")]
fn sample_sns_neurons_report() -> SnsNeuronsReport {
    SnsNeuronsReport {
        schema_version: 1,
        network: "ic".to_string(),
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: SAMPLE_SNS_FETCHED_AT.to_string(),
        source_endpoint: DEFAULT_SNS_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        id: 1,
        name: "Example SNS".to_string(),
        root_canister_id: SAMPLE_SNS_ROOT_CANISTER_ID.to_string(),
        governance_canister_id: SAMPLE_SNS_GOVERNANCE_CANISTER_ID.to_string(),
        requested_limit: 1,
        owner_principal_id: None,
        verbose: false,
        data_source: "api".to_string(),
        sort: SnsNeuronsSort::Api.as_str().to_string(),
        cache_path: None,
        cache_complete: None,
        total_neuron_count: 1,
        neuron_count: 1,
        neurons: vec![SnsNeuronRow {
            neuron_id: "0102030405060708".to_string(),
            cached_neuron_stake_e8s: 100_000_000,
            maturity_e8s_equivalent: 10_000_000,
            staked_maturity_e8s_equivalent: Some(5_000_000),
            created_timestamp_seconds: 1_700_000_000,
            created_at: SAMPLE_SNS_FETCHED_AT.to_string(),
        }],
    }
}

#[cfg(feature = "host")]
fn sample_sns_neurons_refresh_report(
    cache_path: &Path,
    refresh_lock_path: &Path,
    refresh_attempt_path: &Path,
) -> SnsNeuronsRefreshReport {
    SnsNeuronsRefreshReport {
        schema_version: 1,
        network: "ic".to_string(),
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: SAMPLE_SNS_FETCHED_AT.to_string(),
        source_endpoint: DEFAULT_SNS_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        id: 1,
        name: "Example SNS".to_string(),
        root_canister_id: SAMPLE_SNS_ROOT_CANISTER_ID.to_string(),
        governance_canister_id: SAMPLE_SNS_GOVERNANCE_CANISTER_ID.to_string(),
        cache_path: cache_path.display().to_string(),
        refresh_lock_path: refresh_lock_path.display().to_string(),
        refresh_attempt_path: refresh_attempt_path.display().to_string(),
        page_size: 500,
        page_count: 1,
        neuron_count: 1,
        complete: true,
        replaced_existing_cache: false,
        wrote_cache: true,
    }
}

#[cfg(feature = "host")]
fn sample_sns_proposals_refresh_report(
    cache_path: &Path,
    refresh_lock_path: &Path,
    refresh_attempt_path: &Path,
) -> SnsProposalsRefreshReport {
    SnsProposalsRefreshReport {
        schema_version: 1,
        network: "ic".to_string(),
        sns_wasm_canister_id: "qaa6y-5yaaa-aaaaa-aaafa-cai".to_string(),
        fetched_at: SAMPLE_SNS_FETCHED_AT.to_string(),
        source_endpoint: DEFAULT_SNS_SOURCE_ENDPOINT.to_string(),
        fetched_by: "ic-query".to_string(),
        id: 1,
        name: "Example SNS".to_string(),
        root_canister_id: SAMPLE_SNS_ROOT_CANISTER_ID.to_string(),
        governance_canister_id: SAMPLE_SNS_GOVERNANCE_CANISTER_ID.to_string(),
        cache_path: cache_path.display().to_string(),
        refresh_lock_path: refresh_lock_path.display().to_string(),
        refresh_attempt_path: refresh_attempt_path.display().to_string(),
        page_size: 100,
        page_count: 1,
        proposal_count: 1,
        complete: true,
        replaced_existing_cache: false,
        wrote_cache: true,
    }
}

fn sample_sns_proposal_row() -> SnsProposalRow {
    SnsProposalRow {
        proposal_id: Some(42),
        action_id: 7,
        action: "UpgradeSnsControlledCanister".to_string(),
        title: "Upgrade SNS".to_string(),
        summary: "Upgrade the SNS controlled canister.".to_string(),
        url: Some("https://example.com/proposal/42".to_string()),
        decision_state: "open".to_string(),
        status: Some(1),
        topic: Some("governance".to_string()),
        reject_cost_e8s: 100_000_000,
        proposal_creation_timestamp_seconds: 1_700_000_000,
        created_at: "2023-11-14T22:13:20Z".to_string(),
        decided_timestamp_seconds: None,
        decided_at: None,
        executed_timestamp_seconds: None,
        executed_at: None,
        failed_timestamp_seconds: None,
        failed_at: None,
        failure_reason: Some(SnsProposalFailureReason {
            error_type: 0,
            error_message: "none".to_string(),
        }),
        reward_event_round: 12,
        reward_event_end_timestamp_seconds: Some(1_700_086_400),
        is_eligible_for_rewards: true,
        latest_tally: Some(SnsProposalTally {
            timestamp_seconds: 1_700_000_100,
            yes: 100_000_000,
            no: 10_000_000,
            total: 110_000_000,
        }),
        ballot_count: 1,
        ballots: vec![SnsProposalBallotRow {
            neuron_id: "0102030405060708".to_string(),
            vote: 1,
            vote_text: "yes".to_string(),
            cast_timestamp_seconds: 1_700_000_200,
            cast_at: Some("2023-11-14T22:16:40Z".to_string()),
            voting_power: 100_000_000,
        }],
        payload_text_rendering: Some("Upgrade payload".to_string()),
        proposer_neuron_id: Some("010203".to_string()),
    }
}
