//! Unit tests for gRPC methods
use std::{collections::BTreeMap, thread::sleep, time::Duration};

use tonic::transport::Channel;

use zebra_chain::{block::Height, parameters::Network, transaction};
use zebra_test::mock_service::{MockService, PanicAssertion};

use crate::{
    scanner::{
        scanner_client::ScannerClient, ClearResultsRequest, DeleteKeysRequest, Empty,
        GetResultsRequest, GetResultsResponse, InfoReply, KeyWithHeight, RegisterKeysRequest,
        RegisterKeysResponse,
    },
    server::{init, MAX_KEYS_PER_REQUEST},
};
use zebra_node_services::scan_service::{
    request::Request as ScanRequest, response::Response as ScanResponse,
};

/// The extended Sapling viewing key of [ZECpages](https://zecpages.com/boardinfo)
pub const ZECPAGES_SAPLING_VIEWING_KEY: &str = "zxviews1q0duytgcqqqqpqre26wkl45gvwwwd706xw608hucmvfalr759ejwf7qshjf5r9aa7323zulvz6plhttp5mltqcgs9t039cx2d09mgq05ts63n8u35hyv6h9nc9ctqqtue2u7cer2mqegunuulq2luhq3ywjcz35yyljewa4mgkgjzyfwh6fr6jd0dzd44ghk0nxdv2hnv4j5nxfwv24rwdmgllhe0p8568sgqt9ckt02v2kxf5ahtql6s0ltjpkckw8gtymxtxuu9gcr0swvz";

/// Test the gRPC methods with mocked responses
#[tokio::test(flavor = "multi_thread")]
async fn test_grpc_methods_mocked() {
    let _init_guard = zebra_test::init();

    let (client, mock_scan_service) = start_server_and_get_client().await;

    test_get_results_errors(client.clone()).await;
    test_register_keys_errors(client.clone()).await;
    test_clear_results_errors(client.clone()).await;
    test_delete_keys_errors(client.clone()).await;

    for network in Network::iter() {
        test_mocked_getinfo_for_network(&client, &mock_scan_service, &network).await;
        test_mocked_getresults_for_network(&client, &mock_scan_service, &network).await;
        test_mocked_register_keys_for_network(&client, &mock_scan_service, &network).await;
        test_mocked_clear_results_for_network(&client, &mock_scan_service, &network).await;
        test_mocked_delete_keys_for_network(&client, &mock_scan_service, &network).await;
    }
}

/// Test the `get_info` gRPC method with a mocked service response.
async fn test_mocked_getinfo_for_network(
    client: &ScannerClient<Channel>,
    mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
    network: &Network,
) {
    // create request, fake results and get response
    let get_info_response = call_get_info(client.clone(), mock_scan_service.clone(), network).await;

    // test the response
    assert_eq!(
        get_info_response.into_inner().min_sapling_birthday_height,
        network.sapling_activation_height().0,
        "get_info response min sapling height should match network sapling activation height"
    );
}

/// Test the `get_results` gRPC method with populated and empty results
async fn test_mocked_getresults_for_network(
    client: &ScannerClient<Channel>,
    mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
    network: &Network,
) {
    // create request, fake populated results and get response
    let get_results_response =
        call_get_results(client.clone(), mock_scan_service.clone(), network, false).await;

    // test the response
    let transaction_heights = get_results_response
        .into_inner()
        .results
        .first_key_value()
        .expect("should have at least 1 value")
        .1
        .by_height
        .len();
    if network.is_mainnet() {
        assert_eq!(
            transaction_heights, 3,
            "there should be 3 transaction heights"
        );
    } else {
        assert_eq!(
            transaction_heights, 1,
            "there should be 1 transaction height"
        );
    }

    // create request, fake empty results and get response
    let get_results_response =
        call_get_results(client.clone(), mock_scan_service.clone(), network, true).await;

    // test the response
    let is_results_empty = get_results_response
        .into_inner()
        .results
        .first_key_value()
        .expect("should have at least 1 value")
        .1
        .by_height
        .is_empty();

    assert!(is_results_empty, "results should be empty");
}

/// Test the `register_keys` gRPC method
async fn test_mocked_register_keys_for_network(
    client: &ScannerClient<Channel>,
    mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
    network: &Network,
) {
    // create request, fake return value and get response
    let register_keys_response =
        call_register_keys(client.clone(), mock_scan_service.clone(), network).await;

    // test the response
    assert_eq!(
        register_keys_response.into_inner().keys,
        vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()],
        "keys should match mocked response"
    );
}

/// Test the `clear_results` gRPC method
async fn test_mocked_clear_results_for_network(
    client: &ScannerClient<Channel>,
    mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
    network: &Network,
) {
    // create request, fake results and get response
    let get_results_response =
        call_get_results(client.clone(), mock_scan_service.clone(), network, false).await;

    // test the response
    let transaction_heights = get_results_response
        .into_inner()
        .results
        .first_key_value()
        .unwrap()
        .1
        .by_height
        .len();
    if network.is_mainnet() {
        assert_eq!(transaction_heights, 3);
    } else {
        assert_eq!(transaction_heights, 1)
    }

    // create request, fake results and get response
    let clear_results_response =
        call_clear_results(client.clone(), mock_scan_service.clone(), network).await;

    // test the response
    assert_eq!(clear_results_response.into_inner(), Empty {});

    // create request, fake results and get response
    let get_results_response =
        call_get_results(client.clone(), mock_scan_service.clone(), network, true).await;

    // test the response
    let is_results_empty = get_results_response
        .into_inner()
        .results
        .first_key_value()
        .unwrap()
        .1
        .by_height
        .is_empty();

    assert!(is_results_empty, "results should be empty");
}

/// Test the `delete_keys` gRPC method
async fn test_mocked_delete_keys_for_network(
    client: &ScannerClient<Channel>,
    mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
    network: &Network,
) {
    // create request, fake results and get response
    let register_keys_response =
        call_register_keys(client.clone(), mock_scan_service.clone(), network).await;

    // test the response
    assert_eq!(
        register_keys_response.into_inner().keys,
        vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()]
    );

    // create request, fake results and get response
    let get_results_response =
        call_get_results(client.clone(), mock_scan_service.clone(), network, false).await;
    let transaction_heights = get_results_response
        .into_inner()
        .results
        .first_key_value()
        .unwrap()
        .1
        .by_height
        .len();
    if network.is_mainnet() {
        assert_eq!(transaction_heights, 3);
    } else {
        assert_eq!(transaction_heights, 1);
    }

    let delete_keys_response =
        call_delete_keys(client.clone(), mock_scan_service.clone(), network).await;
    // test the response
    assert_eq!(delete_keys_response.into_inner(), Empty {});

    let get_results_response =
        call_get_results(client.clone(), mock_scan_service.clone(), network, true).await;
    let is_results_empty = get_results_response
        .into_inner()
        .results
        .first_key_value()
        .unwrap()
        .1
        .by_height
        .is_empty();

    assert!(is_results_empty, "results should be empty");
}

/// Start the gRPC server, get a client and a mock service
async fn start_server_and_get_client() -> (
    ScannerClient<tonic::transport::Channel>,
    MockService<ScanRequest, ScanResponse, PanicAssertion>,
) {
    // get a mocked scan service
    let mock_scan_service = MockService::build()
        .with_max_request_delay(Duration::from_secs(2))
        .for_unit_tests();

    // start the gRPC server
    let listen_addr: std::net::SocketAddr = "127.0.0.1:0"
        .parse()
        .expect("hard-coded IP and u16 port should parse successfully");

    let (_server_task, listen_addr) = init(listen_addr, mock_scan_service.clone())
        .await
        .expect("Possible port conflict");

    // wait for the server to start
    sleep(Duration::from_secs(1));

    let endpoint = tonic::transport::channel::Endpoint::new(format!("http://{listen_addr}"))
        .unwrap()
        .timeout(Duration::from_secs(2));

    // connect to the gRPC server
    let client = ScannerClient::connect(endpoint)
        .await
        .expect("server should receive connection");

    (client, mock_scan_service)
}

/// Add fake populated results to the mock scan service
async fn add_fake_populated_results(
    mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
    network: &Network,
) {
    let mut mock_scan_service = mock_scan_service.clone();
    let network = network.clone();
    tokio::spawn(async move {
        let zec_pages_sapling_efvk = ZECPAGES_SAPLING_VIEWING_KEY.to_string();
        let mut fake_results = BTreeMap::new();
        let heights = if network.is_mainnet() {
            vec![Height::MIN, Height(1), Height::MAX]
        } else {
            vec![Height::MIN]
        };
        for fake_result_height in heights {
            fake_results.insert(
                fake_result_height,
                [transaction::Hash::from([0; 32])].repeat(3),
            );
        }

        let mut fake_results_response = BTreeMap::new();
        fake_results_response.insert(zec_pages_sapling_efvk, fake_results);

        mock_scan_service
            .expect_request_that(|req| matches!(req, ScanRequest::Results(_)))
            .await
            .respond(ScanResponse::Results(fake_results_response))
    });
}

/// Add fake empty results to the mock scan service
async fn add_fake_empty_results(
    mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
    _network: &Network,
) {
    let mut mock_scan_service = mock_scan_service.clone();
    tokio::spawn(async move {
        let zec_pages_sapling_efvk = ZECPAGES_SAPLING_VIEWING_KEY.to_string();
        let fake_results = BTreeMap::new();
        let mut fake_results_response = BTreeMap::new();
        fake_results_response.insert(zec_pages_sapling_efvk, fake_results);

        mock_scan_service
            .expect_request_that(|req| matches!(req, ScanRequest::Results(_)))
            .await
            .respond(ScanResponse::Results(fake_results_response))
    });
}

/// Call the `get_results` gRPC method, mock and return the response
async fn call_get_results(
    client: ScannerClient<Channel>,
    mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
    network: &Network,
    empty_results: bool,
) -> tonic::Response<GetResultsResponse> {
    let get_results_response_fut = {
        let mut client = client.clone();
        let get_results_request = tonic::Request::new(GetResultsRequest {
            keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()],
        });
        tokio::spawn(async move { client.get_results(get_results_request).await })
    };

    if empty_results {
        add_fake_empty_results(mock_scan_service, network).await;
    } else {
        add_fake_populated_results(mock_scan_service, network).await;
    }

    get_results_response_fut
        .await
        .expect("tokio task should join successfully")
        .expect("get_results request should succeed")
}

/// Calls the `get_results` gRPC method with bad request data and asserts that it returns errors
async fn test_get_results_errors(mut client: ScannerClient<Channel>) {
    let request = tonic::Request::new(GetResultsRequest { keys: vec![] });
    let response = client.get_results(request).await;

    let response_error_code = response
        .expect_err("calling get_results with no keys should return an error")
        .code();

    assert_eq!(
        response_error_code,
        tonic::Code::InvalidArgument,
        "error code should be an invalid argument error"
    );

    let request = tonic::Request::new(GetResultsRequest {
        keys: vec![
            ZECPAGES_SAPLING_VIEWING_KEY.to_string();
            MAX_KEYS_PER_REQUEST
                .checked_add(1)
                .expect("should fit in usize")
        ],
    });
    let response = client.get_results(request).await;
    let response_error_code = response
        .expect_err("calling get_results too with many keys should return an error")
        .code();

    assert_eq!(
        response_error_code,
        tonic::Code::InvalidArgument,
        "error code should be an invalid argument error"
    );
}

/// Call the `get_info` gRPC method, mock and return the response
async fn call_get_info(
    client: ScannerClient<Channel>,
    mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
    network: &Network,
) -> tonic::Response<InfoReply> {
    let get_info_response_fut = {
        let mut client = client.clone();
        let get_info_request = tonic::Request::new(Empty {});
        tokio::spawn(async move { client.get_info(get_info_request).await })
    };

    let mut mock_scan_service = mock_scan_service.clone();
    let network = network.clone();
    tokio::spawn(async move {
        mock_scan_service
            .expect_request_that(|req| matches!(req, ScanRequest::Info))
            .await
            .respond(ScanResponse::Info {
                min_sapling_birthday_height: network.sapling_activation_height(),
            })
    });

    get_info_response_fut
        .await
        .expect("tokio task should join successfully")
        .expect("get_info request should succeed")
}

/// Call the `register_keys` gRPC method, mock and return the response
async fn call_register_keys(
    client: ScannerClient<Channel>,
    mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
    _network: &Network,
) -> tonic::Response<RegisterKeysResponse> {
    let key_with_height = KeyWithHeight {
        key: ZECPAGES_SAPLING_VIEWING_KEY.to_string(),
        height: None,
    };

    let register_keys_response_fut = {
        let mut client = client.clone();
        let register_keys_request = tonic::Request::new(RegisterKeysRequest {
            keys: vec![key_with_height],
        });
        tokio::spawn(async move { client.register_keys(register_keys_request).await })
    };

    let mut mock_scan_service = mock_scan_service.clone();
    tokio::spawn(async move {
        mock_scan_service
            .expect_request_that(|req| matches!(req, ScanRequest::RegisterKeys(_)))
            .await
            .respond(ScanResponse::RegisteredKeys(vec![
                ZECPAGES_SAPLING_VIEWING_KEY.to_string(),
            ]))
    });

    register_keys_response_fut
        .await
        .expect("tokio task should join successfully")
        .expect("register_keys request should succeed")
}

/// Calls the `register_keys` gRPC method with bad request data and asserts that it returns errors
async fn test_register_keys_errors(client: ScannerClient<Channel>) {
    let key_with_height = KeyWithHeight {
        key: ZECPAGES_SAPLING_VIEWING_KEY.to_string(),
        height: None,
    };

    let fut = {
        let mut client = client.clone();
        let request = tonic::Request::new(RegisterKeysRequest { keys: vec![] });
        tokio::spawn(async move { client.register_keys(request).await })
    };

    let response = fut.await.expect("tokio task should join successfully");
    assert!(response.is_err());
    assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);

    let fut = {
        let mut client = client.clone();
        let request = tonic::Request::new(RegisterKeysRequest {
            keys: vec![key_with_height; 11],
        });
        tokio::spawn(async move { client.register_keys(request).await })
    };

    let response = fut.await.expect("tokio task should join successfully");
    assert!(response.is_err());
    assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);
}

async fn call_clear_results(
    client: ScannerClient<Channel>,
    mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
    _network: &Network,
) -> tonic::Response<Empty> {
    let clear_results_response_fut = {
        let mut client = client.clone();
        let clear_results_request = tonic::Request::new(ClearResultsRequest {
            keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()],
        });
        tokio::spawn(async move { client.clear_results(clear_results_request).await })
    };

    let mut mock_scan_service = mock_scan_service.clone();
    tokio::spawn(async move {
        mock_scan_service
            .expect_request_that(|req| matches!(req, ScanRequest::ClearResults(_)))
            .await
            .respond(ScanResponse::ClearedResults)
    });

    clear_results_response_fut
        .await
        .expect("tokio task should join successfully")
        .expect("register_keys request should succeed")
}

/// Calls the `clear_results` gRPC method with bad request data and asserts that it returns errors
async fn test_clear_results_errors(client: ScannerClient<Channel>) {
    let fut = {
        let mut client = client.clone();
        let request = tonic::Request::new(ClearResultsRequest { keys: vec![] });
        tokio::spawn(async move { client.clear_results(request).await })
    };

    let response = fut.await.expect("tokio task should join successfully");
    assert!(response.is_err());
    assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);

    let fut = {
        let mut client = client.clone();
        let request = tonic::Request::new(ClearResultsRequest {
            keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string(); 11],
        });
        tokio::spawn(async move { client.clear_results(request).await })
    };

    let response = fut.await.expect("tokio task should join successfully");
    assert!(response.is_err());
    assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);
}

/// Call the `delete_keys` gRPC method, mock and return the response
async fn call_delete_keys(
    client: ScannerClient<Channel>,
    mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
    _network: &Network,
) -> tonic::Response<Empty> {
    let delete_keys_response_fut = {
        let mut client = client.clone();
        let delete_keys_request = tonic::Request::new(DeleteKeysRequest {
            keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()],
        });
        tokio::spawn(async move { client.delete_keys(delete_keys_request).await })
    };

    let mut mock_scan_service = mock_scan_service.clone();
    tokio::spawn(async move {
        mock_scan_service
            .expect_request_that(|req| matches!(req, ScanRequest::DeleteKeys(_)))
            .await
            .respond(ScanResponse::DeletedKeys)
    });
    delete_keys_response_fut
        .await
        .expect("tokio task should join successfully")
        .expect("delete_keys request should succeed")
}

/// Calls the `delete_keys` gRPC method with bad request data and asserts that it returns errors
async fn test_delete_keys_errors(client: ScannerClient<Channel>) {
    let fut = {
        let mut client = client.clone();
        let request = tonic::Request::new(DeleteKeysRequest { keys: vec![] });
        tokio::spawn(async move { client.delete_keys(request).await })
    };

    let response = fut.await.expect("tokio task should join successfully");
    assert!(response.is_err());
    assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);

    let fut = {
        let mut client = client.clone();
        let request = tonic::Request::new(DeleteKeysRequest {
            keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string(); 11],
        });
        tokio::spawn(async move { client.delete_keys(request).await })
    };

    let response = fut.await.expect("tokio task should join successfully");
    assert!(response.is_err());
    assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);
}
