// 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, Context as _, Error},
    ethernet,
    fidl_fuchsia_netemul_network::{
        EndpointManagerMarker, FakeEndpointMarker, NetworkContextMarker, NetworkManagerMarker,
    },
    fidl_fuchsia_netemul_sync::{BusMarker, BusProxy, SyncManagerMarker},
    fidl_fuchsia_netstack::{InterfaceConfig, NetstackMarker},
    fuchsia_async as fasync,
    fuchsia_component::client,
    fuchsia_zircon as zx,
    futures::{self, TryStreamExt},
    std::{str, task::Poll},
    structopt::StructOpt,
};

#[derive(StructOpt, Debug)]
struct Opt {
    #[structopt(long = "mock_guest")]
    is_mock_guest: bool,
    #[structopt(long = "server")]
    is_server: bool,
    #[structopt(long)]
    network_name: Option<String>,
    #[structopt(long)]
    endpoint_name: Option<String>,
    #[structopt(long)]
    server_name: Option<String>,
}

const DEFAULT_METRIC: u32 = 100;
const BUS_NAME: &'static str = "netstack-itm-bus";

fn open_bus(cli_name: &str) -> Result<BusProxy, Error> {
    let syncm = client::connect_to_protocol::<SyncManagerMarker>()?;
    let (bus, bus_server_end) = fidl::endpoints::create_proxy::<BusMarker>()?;
    syncm.bus_subscribe(BUS_NAME, cli_name, bus_server_end)?;
    Ok(bus)
}

async fn run_mock_guest(
    network_name: String,
    ep_name: String,
    server_name: String,
) -> Result<(), Error> {
    // Create an ethertap client and an associated ethernet device.
    let ctx = client::connect_to_protocol::<NetworkContextMarker>()?;
    let (epm, epm_server_end) = fidl::endpoints::create_proxy::<EndpointManagerMarker>()?;
    ctx.get_endpoint_manager(epm_server_end)?;
    let (netm, netm_server_end) = fidl::endpoints::create_proxy::<NetworkManagerMarker>()?;
    ctx.get_network_manager(netm_server_end)?;

    let ep = epm.get_endpoint(&ep_name).await?.unwrap().into_proxy()?;
    let net = netm.get_network(&network_name).await?.unwrap().into_proxy()?;
    let (fake_ep, fake_ep_server_end) = fidl::endpoints::create_proxy::<FakeEndpointMarker>()?;
    net.create_fake_endpoint(fake_ep_server_end)?;

    let netstack = client::connect_to_protocol::<NetstackMarker>()?;
    let mut cfg = InterfaceConfig {
        name: "eth-test".to_string(),
        filepath: "[TBD]".to_string(),
        metric: DEFAULT_METRIC,
    };

    let _nicid = match ep.get_device().await? {
        fidl_fuchsia_netemul_network::DeviceConnection::Ethernet(eth_device) => {
            netstack.add_ethernet_device(&format!("/{}", ep_name), &mut cfg, eth_device).await?
        }
        fidl_fuchsia_netemul_network::DeviceConnection::NetworkDevice(netdevice) => {
            todo!("(48860) Support and test for NetworkDevice connections. Got unexpected NetworkDevice {:?}", netdevice)
        }
    };

    // Send a message to the server and expect it to be echoed back.
    let echo_string = String::from("hello");

    let bus = open_bus(&ep_name)?;
    let (success, absent) =
        bus.wait_for_clients(&mut vec![server_name.as_str()].drain(..), 0).await?;
    assert!(success);
    assert_eq!(absent, None);

    fake_ep.write(echo_string.as_bytes()).await.context("write failed")?;

    println!("To Server: {}", echo_string);

    let (data, dropped_frames) = fake_ep.read().await.context("read failed")?;
    assert_eq!(dropped_frames, 0);
    let server_string = str::from_utf8(&data)?;
    assert!(
        echo_string == server_string,
        "Server reply ({}) did not match client message ({})",
        server_string,
        echo_string
    );
    println!("From Server: {}", server_string);

    Ok(())
}

async fn run_echo_server_ethernet(
    ep_name: String,
    eth_dev: fidl::endpoints::ClientEnd<fidl_fuchsia_hardware_ethernet::DeviceMarker>,
) -> Result<(), Error> {
    // Create an EthernetClient to wrap around the Endpoint's ethernet device.
    let vmo = zx::Vmo::create(256 * ethernet::DEFAULT_BUFFER_SIZE as u64)?;

    let eth_proxy = match eth_dev.into_proxy() {
        Ok(proxy) => proxy,
        _ => return Err(format_err!("Could not get ethernet proxy")),
    };

    let mut eth_client =
        ethernet::Client::new(eth_proxy, vmo, ethernet::DEFAULT_BUFFER_SIZE, &ep_name).await?;

    eth_client.start().await?;

    // Listen for a receive event from the client, echo back the client's
    // message, and then exit.
    let mut eth_events = eth_client.get_stream();
    let mut sent_response = false;

    // Before connecting to the message bus to notify the client of the server's existence, poll
    // for events.  Buffers will not be allocated until polling is performed so this ensures that
    // there will be buffers to receive the client's message.
    loop {
        match futures::poll!(eth_events.try_next()) {
            Poll::Pending => break,
            Poll::Ready(result) => match result {
                Ok(result) => match result {
                    Some(_) => continue,
                    None => panic!("event stream produced empty event"),
                },
                Err(e) => panic!("event stream returned an error: {}", e),
            },
        }
    }

    // get on bus to unlock mock_guest part of test
    let _bus = open_bus(&ep_name)?;

    while let Some(event) = eth_events.try_next().await? {
        match event {
            ethernet::Event::Receive(rx, _flags) => {
                if !sent_response {
                    let mut data: [u8; 100] = [0; 100];
                    let sz = rx.read(&mut data);
                    let user_message =
                        str::from_utf8(&data[0..sz]).expect("failed to parse string");
                    println!("From client: {}", user_message);
                    let () = eth_client.send(&data[0..sz]);
                    sent_response = true;

                    // Start listening for the server's response to be
                    // transmitted to the guest.
                    eth_client.tx_listen_start().await?;
                } else {
                    // The mock guest will not send anything to the server
                    // beyond its initial request.  After the server has echoed
                    // the response, the next received message will be the
                    // server's own output since it is listening for its own
                    // Tx messages.
                    break;
                }
            }
            _ => {
                continue;
            }
        }
    }

    Ok(())
}

async fn run_echo_server(ep_name: String) -> Result<(), Error> {
    // Get the Endpoint that was created in the server's environment.
    let netctx = client::connect_to_protocol::<NetworkContextMarker>()?;
    let (epm, epm_server_end) = fidl::endpoints::create_proxy::<EndpointManagerMarker>()?;
    netctx.get_endpoint_manager(epm_server_end)?;

    let ep = epm.get_endpoint(&ep_name).await?;

    let ep = match ep {
        Some(ep) => ep.into_proxy()?,
        None => return Err(format_err!("Can't find endpoint {}", &ep_name)),
    };

    match ep.get_device().await? {
        fidl_fuchsia_netemul_network::DeviceConnection::Ethernet(e) => {
            run_echo_server_ethernet(ep_name, e).await
        }
        fidl_fuchsia_netemul_network::DeviceConnection::NetworkDevice(netdevice) => {
            todo!("(48860) Support and test for NetworkDevice connections. Got unexpected NetworkDevice {:?}", netdevice)
        }
    }
}

#[fasync::run_singlethreaded]
async fn main() -> Result<(), Error> {
    let opt = Opt::from_args();

    if opt.is_mock_guest {
        if opt.network_name == None || opt.endpoint_name == None || opt.server_name == None {
            return Err(format_err!(
                "Must provide network_name, endpoint_name, and server_name for mock guests"
            ));
        }
        run_mock_guest(
            opt.network_name.unwrap(),
            opt.endpoint_name.unwrap(),
            opt.server_name.unwrap(),
        )
        .await?;
    } else if opt.is_server {
        match opt.endpoint_name {
            Some(endpoint_name) => {
                run_echo_server(endpoint_name).await?;
            }
            None => {
                return Err(format_err!("Must provide endpoint_name for server"));
            }
        }
    }
    Ok(())
}
