// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    anyhow::{format_err, Error},
    fidl::endpoints::{create_endpoints, ServerEnd},
    fidl_fuchsia_bluetooth::{ConnectionRole, Uuid},
    fidl_fuchsia_bluetooth_le::{
        AdvertisingData, AdvertisingHandleMarker, AdvertisingModeHint, AdvertisingParameters,
        PeripheralError, PeripheralStartAdvertisingResult as AdvertisingResult,
    },
    fidl_fuchsia_bluetooth_test::{
        ConnectionState, HciEmulatorProxy, LegacyAdvertisingType, LowEnergyPeerParameters,
        PeerProxy, MAX_LEGACY_ADVERTISING_DATA_LENGTH,
    },
    fuchsia_async::{self as fasync, DurationExt, TimeoutExt},
    fuchsia_bluetooth::{
        expectation::asynchronous::{ExpectableState, ExpectableStateExt},
        types::Address,
    },
    futures::{Future, Stream, TryFutureExt, TryStream, TryStreamExt},
    std::{iter::repeat, mem::drop},
};

use crate::{
    harness::{
        emulator,
        expect::{expect_eq, expect_ok},
        low_energy_peripheral::PeripheralHarness,
    },
    tests::timeout_duration,
};

mod expectation {
    use {
        crate::harness::low_energy_peripheral::PeripheralState,
        fuchsia_bluetooth::expectation::Predicate,
    };

    pub fn peripheral_received_connection() -> Predicate<PeripheralState> {
        Predicate::new(
            move |state: &PeripheralState| -> bool { !state.connections.is_empty() },
            Some("le.Peripheral notified a connection"),
        )
    }
}

fn empty_advertising_data() -> AdvertisingData {
    AdvertisingData {
        name: None,
        appearance: None,
        tx_power_level: None,
        service_uuids: None,
        service_data: None,
        manufacturer_data: None,
        uris: None,
    }
}

async fn start_advertising(
    harness: &PeripheralHarness,
    params: AdvertisingParameters,
    handle: ServerEnd<AdvertisingHandleMarker>,
) -> Result<AdvertisingResult, Error> {
    let fut = harness
        .aux()
        .proxy()
        .start_advertising(params, handle)
        .map_err(|e| e.into())
        .on_timeout(timeout_duration().after_now(), move || Err(format_err!("timed out")));
    fut.await.map_err(|e| e.context("Could not start advertising").into())
}

fn default_parameters() -> AdvertisingParameters {
    AdvertisingParameters {
        data: None,
        scan_response: None,
        mode_hint: None,
        connectable: None,
        connection_options: None,
    }
}

async fn test_enable_advertising(harness: PeripheralHarness) -> Result<(), Error> {
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, default_parameters(), handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;

    let _ = harness
        .when_satisfied(emulator::expectation::advertising_is_enabled(true), timeout_duration())
        .await?;
    Ok(())
}

async fn test_enable_and_disable_advertising(harness: PeripheralHarness) -> Result<(), Error> {
    let (handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, default_parameters(), handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(emulator::expectation::advertising_is_enabled(true), timeout_duration())
        .await?;

    // Closing the advertising handle should stop advertising.
    drop(handle);
    let _ = harness
        .when_satisfied(emulator::expectation::advertising_is_enabled(false), timeout_duration())
        .await?;
    Ok(())
}

async fn test_advertising_handle_closed_while_pending(
    harness: PeripheralHarness,
) -> Result<(), Error> {
    let (handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;

    // Drop the handle before getting a response to abort the procedure.
    drop(handle);
    let result = start_advertising(&harness, default_parameters(), handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;

    // Advertising should become disabled after getting enabled once.
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_was_enabled(true)
                .and(emulator::expectation::advertising_is_enabled(false)),
            timeout_duration(),
        )
        .await?;
    Ok(())
}

async fn test_advertising_data_too_long(harness: PeripheralHarness) -> Result<(), Error> {
    const LENGTH: usize = (MAX_LEGACY_ADVERTISING_DATA_LENGTH + 1) as usize;
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;

    // Assign a very long name.
    let mut params = default_parameters();
    params.data = Some(AdvertisingData {
        name: Some(repeat("x").take(LENGTH).collect::<String>()),
        ..empty_advertising_data()
    });
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_eq!(Err(PeripheralError::AdvertisingDataTooLong), result)?;
    Ok(())
}

async fn test_scan_response_data_too_long(harness: PeripheralHarness) -> Result<(), Error> {
    const LENGTH: usize = (MAX_LEGACY_ADVERTISING_DATA_LENGTH + 1) as usize;
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;

    // Assign a very long name.
    let mut params = default_parameters();
    params.scan_response = Some(AdvertisingData {
        name: Some(repeat("x").take(LENGTH).collect::<String>()),
        ..empty_advertising_data()
    });
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_eq!(Err(PeripheralError::ScanResponseDataTooLong), result)?;
    Ok(())
}

async fn test_update_advertising(harness: PeripheralHarness) -> Result<(), Error> {
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, default_parameters(), handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(emulator::expectation::advertising_is_enabled(true), timeout_duration())
        .await?;
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_type_is(LegacyAdvertisingType::AdvNonconnInd),
            timeout_duration(),
        )
        .await?;
    harness.write_state().reset();

    // Call `start_advertising` again with new parameters.
    let mut params = default_parameters();
    params.connectable = Some(true);
    let (_handle2, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;

    // Advertising should stop and start with the new parameters.
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_was_enabled(false)
                .and(emulator::expectation::advertising_is_enabled(true)),
            timeout_duration(),
        )
        .await?;
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_type_is(LegacyAdvertisingType::AdvInd),
            timeout_duration(),
        )
        .await?;

    Ok(())
}

async fn test_advertising_types(harness: PeripheralHarness) -> Result<(), Error> {
    // Non-connectable
    let params = AdvertisingParameters { connectable: Some(false), ..default_parameters() };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_type_is(LegacyAdvertisingType::AdvNonconnInd),
            timeout_duration(),
        )
        .await?;

    // Connectable
    let params = AdvertisingParameters { connectable: Some(true), ..default_parameters() };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_type_is(LegacyAdvertisingType::AdvInd),
            timeout_duration(),
        )
        .await?;

    // Scannable
    let params = AdvertisingParameters {
        connectable: Some(false),
        scan_response: Some(AdvertisingData {
            name: Some("hello".to_string()),
            ..empty_advertising_data()
        }),
        ..default_parameters()
    };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_type_is(LegacyAdvertisingType::AdvScanInd),
            timeout_duration(),
        )
        .await?;

    // Connectable and scannable
    let params = AdvertisingParameters {
        connectable: Some(true),
        scan_response: Some(AdvertisingData {
            name: Some("hello".to_string()),
            ..empty_advertising_data()
        }),
        ..default_parameters()
    };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_type_is(LegacyAdvertisingType::AdvInd),
            timeout_duration(),
        )
        .await?;

    Ok(())
}

async fn test_advertising_modes(harness: PeripheralHarness) -> Result<(), Error> {
    // Very fast advertising interval (<= 60 ms), only supported for connectable advertising.
    let params = AdvertisingParameters {
        connectable: Some(true),
        mode_hint: Some(AdvertisingModeHint::VeryFast),
        ..default_parameters()
    };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(emulator::expectation::advertising_max_interval_is(60), timeout_duration())
        .await?;

    // Very fast advertising interval (<= 60 ms) falls back to "fast" parameters for non-connectable
    // advertising.
    let params = AdvertisingParameters {
        mode_hint: Some(AdvertisingModeHint::VeryFast),
        ..default_parameters()
    };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(emulator::expectation::advertising_max_interval_is(150), timeout_duration())
        .await?;

    // Fast advertising interval (<= 150 ms)
    let params = AdvertisingParameters {
        mode_hint: Some(AdvertisingModeHint::Fast),
        ..default_parameters()
    };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(emulator::expectation::advertising_max_interval_is(150), timeout_duration())
        .await?;

    // Slow advertising interval (<= 1.2 s)
    let params = AdvertisingParameters {
        mode_hint: Some(AdvertisingModeHint::Slow),
        ..default_parameters()
    };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_max_interval_is(1200),
            timeout_duration(),
        )
        .await?;

    Ok(())
}

async fn test_advertising_data(harness: PeripheralHarness) -> Result<(), Error> {
    // Test that encoding one field works. The serialization of other fields is unit tested elsewhere.
    let params = AdvertisingParameters {
        data: Some(AdvertisingData { name: Some("hello".to_string()), ..empty_advertising_data() }),
        ..default_parameters()
    };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;

    let data = vec![
        // Flags (General discoverable mode)
        0x02,
        0x01,
        0x02,
        // The local name, as above.
        0x06,
        0x09,
        ('h' as u8),
        ('e' as u8),
        ('l' as u8),
        ('l' as u8),
        ('o' as u8),
    ];
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_is_enabled(true)
                .and(emulator::expectation::advertising_data_is(data)),
            timeout_duration(),
        )
        .await?;

    Ok(())
}

async fn test_scan_response(harness: PeripheralHarness) -> Result<(), Error> {
    // Test that encoding one field works. The serialization of other fields is unit tested elsewhere.
    let params = AdvertisingParameters {
        data: Some(AdvertisingData {
            service_uuids: Some(vec![Uuid {
                value: [
                    0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x0d,
                    0x18, 0x00, 0x00,
                ],
            }]),
            ..empty_advertising_data()
        }),
        scan_response: Some(AdvertisingData {
            name: Some("hello".to_string()),
            ..empty_advertising_data()
        }),
        ..default_parameters()
    };
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;
    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;

    let data = vec![
        0x02, 0x01, 0x02, // Flags (General discoverable mode)
        0x03, 0x02, 0x0d, 0x18, // Incomplete list of service UUIDs
    ];
    let scan_rsp = vec![
        // The local name, as above.
        0x06,
        0x09,
        ('h' as u8),
        ('e' as u8),
        ('l' as u8),
        ('l' as u8),
        ('o' as u8),
    ];
    let _ = harness
        .when_satisfied(
            emulator::expectation::advertising_is_enabled(true)
                .and(emulator::expectation::advertising_data_is(data))
                .and(emulator::expectation::scan_response_is(scan_rsp)),
            timeout_duration(),
        )
        .await?;

    Ok(())
}

fn default_address() -> Address {
    Address::Public([1, 0, 0, 0, 0, 0])
}

async fn add_fake_peer(proxy: &HciEmulatorProxy, address: &Address) -> Result<PeerProxy, Error> {
    let (local, remote) = fidl::endpoints::create_proxy()?;
    let params = LowEnergyPeerParameters {
        address: Some(address.into()),
        connectable: Some(true),
        advertisement: None,
        scan_response: None,
    };
    let _ = proxy
        .add_low_energy_peer(params, remote)
        .await?
        .map_err(|e| format_err!("Failed to register fake peer: {:?}", e))?;
    Ok(local)
}

// Returns success when the `events` stream returns a "None" or fails. (This is what FIDL handles do
// when their peer gets closed). Returns an error if the timeout expires before then.
fn watch_handle_closed<T, V>(mut events: T) -> impl Future<Output = Result<(), Error>>
where
    T: Stream<Item = Result<V, fidl::Error>>
        + Send
        + std::marker::Unpin
        + TryStream<Error = fidl::Error>
        + TryStreamExt,
{
    let f = async move {
        while events.try_next().await?.is_some() {}
        Ok(())
    };
    f.on_timeout(timeout_duration().after_now(), || {
        Err(format_err!("Timed out before handle closed"))
    })
}

async fn test_receive_connection(harness: PeripheralHarness) -> Result<(), Error> {
    let emulator = harness.aux().emulator().clone();
    let address = default_address();
    let peer = add_fake_peer(&emulator, &address).await?;
    let (handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;

    let mut params = default_parameters();
    params.connectable = Some(true);

    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(emulator::expectation::advertising_is_enabled(true), timeout_duration())
        .await?;

    peer.emulate_le_connection_complete(ConnectionRole::Follower)?;
    let _ = harness
        .when_satisfied(expectation::peripheral_received_connection(), timeout_duration())
        .await?;

    // Receiving a connection is expected to stop advertising. Verify that the emulator no longer
    // advertises.
    let _ = harness
        .when_satisfied(emulator::expectation::advertising_is_enabled(false), timeout_duration())
        .await?;

    // Similarly our AdvertisingHandle should be closed by the system. Polling for events should
    // result in an error. Keep a local copy to ensure that the handle doesn't get dropped
    // locally.
    let handle = handle.into_proxy()?;
    watch_handle_closed(handle.clone().take_event_stream()).await
}

async fn test_connection_dropped_when_not_connectable(
    harness: PeripheralHarness,
) -> Result<(), Error> {
    let emulator = harness.aux().emulator().clone();
    let address = default_address();
    let peer = add_fake_peer(&emulator, &address).await?;
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;

    // `default_parameters()` are configured as non-connectable.
    let result = start_advertising(&harness, default_parameters(), handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;
    let _ = harness
        .when_satisfied(emulator::expectation::advertising_is_enabled(true), timeout_duration())
        .await?;

    peer.emulate_le_connection_complete(ConnectionRole::Follower)?;

    // Wait for the connection to get dropped by the stack as it should be rejected when we are not
    // connectable. We assign our own PeerId here for tracking purposes (this is distinct from the
    // PeerId that the Peripheral proxy would report).
    fasync::spawn(
        emulator::watch_peer_connection_states(harness.clone(), address, peer.clone())
            .unwrap_or_else(|_| ()),
    );

    let _ = harness
        .when_satisfied(
            emulator::expectation::peer_connection_state_was(address, ConnectionState::Connected)
                .and(emulator::expectation::peer_connection_state_is(
                    address,
                    ConnectionState::Disconnected,
                )),
            timeout_duration(),
        )
        .await?;

    // Make sure that we haven't received any connection events over the Peripheral protocol.
    expect_true!(harness.read().connections.is_empty())?;

    Ok(())
}

async fn test_drop_connection(harness: PeripheralHarness) -> Result<(), Error> {
    let emulator = harness.aux().emulator().clone();
    let address = default_address();
    let peer = add_fake_peer(&emulator, &address).await?;
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;

    let mut params = default_parameters();
    params.connectable = Some(true);

    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;

    peer.emulate_le_connection_complete(ConnectionRole::Follower)?;
    fasync::spawn(
        emulator::watch_peer_connection_states(harness.clone(), address, peer.clone())
            .unwrap_or_else(|_| ()),
    );

    let _ = harness
        .when_satisfied(expectation::peripheral_received_connection(), timeout_duration())
        .await?;

    expect_true!(harness.read().connections.len() == 1)?;
    let (_, conn) = harness.write_state().connections.remove(0);

    // Explicitly drop the connection handle. This should tell the emulator to disconnect the peer.
    drop(conn);
    let _ = harness
        .when_satisfied(
            emulator::expectation::peer_connection_state_was(address, ConnectionState::Connected)
                .and(emulator::expectation::peer_connection_state_is(
                    address,
                    ConnectionState::Disconnected,
                )),
            timeout_duration(),
        )
        .await?;

    Ok(())
}

async fn test_connection_handle_closes_on_disconnect(
    harness: PeripheralHarness,
) -> Result<(), Error> {
    let emulator = harness.aux().emulator().clone();
    let address = default_address();
    let peer = add_fake_peer(&emulator, &address).await?;
    let (_handle, handle_remote) = create_endpoints::<AdvertisingHandleMarker>()?;

    let mut params = default_parameters();
    params.connectable = Some(true);

    let result = start_advertising(&harness, params, handle_remote).await?;
    expect_ok(result, "failed to start advertising")?;

    peer.emulate_le_connection_complete(ConnectionRole::Follower)?;
    fasync::spawn(
        emulator::watch_peer_connection_states(harness.clone(), address, peer.clone())
            .unwrap_or_else(|_| ()),
    );

    let _ = harness
        .when_satisfied(expectation::peripheral_received_connection(), timeout_duration())
        .await?;

    expect_true!(harness.read().connections.len() == 1)?;
    let (_, conn) = harness.write_state().connections.remove(0);

    // Tell the controller to disconnect the link. The harness should get notified of this.
    peer.emulate_disconnection_complete()?;
    let _ = harness
        .when_satisfied(
            emulator::expectation::peer_connection_state_was(address, ConnectionState::Connected)
                .and(emulator::expectation::peer_connection_state_is(
                    address,
                    ConnectionState::Disconnected,
                )),
            timeout_duration(),
        )
        .await?;

    // Our connection handle should be closed by the system. Polling for events should
    // result in an error. Keep a local copy to ensure that the handle doesn't get dropped
    // locally.
    watch_handle_closed(conn.clone().take_event_stream()).await
}

/// Run all test cases.
pub fn run_all() -> Result<(), Error> {
    run_suite!(
        "le.Peripheral",
        [
            test_enable_advertising,
            test_enable_and_disable_advertising,
            test_advertising_handle_closed_while_pending,
            test_advertising_data_too_long,
            test_scan_response_data_too_long,
            test_update_advertising,
            test_advertising_types,
            test_advertising_modes,
            test_advertising_data,
            test_scan_response,
            test_receive_connection,
            test_connection_dropped_when_not_connectable,
            test_drop_connection,
            test_connection_handle_closes_on_disconnect
        ]
    )
}
