// Copyright 2020 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::{anyhow, Context, Error};
use async_trait::async_trait;
use component_events::{injectors::*, matcher::EventMatcher};
use fidl::endpoints::ServerEnd;
use fidl_componentmanager_test as ftest;
use fidl_fuchsia_io as fio;
use fuchsia_async as fasync;
use fuchsia_zircon as zx;
use futures::{channel::oneshot, lock::Mutex, prelude::*};
use log::*;
use std::sync::Arc;
use test_util::assert_geq;
use test_utils_lib::opaque_test::OpaqueTestBuilder;
use vfs::{
    directory::entry::DirectoryEntry, execution_scope::ExecutionScope, file::vmo::read_only_static,
    pseudo_directory,
};

/// Offset the maintainer component uses from backstop to set the UTC time.
const TEST_OFFSET: zx::Duration = zx::Duration::from_minutes(2);

#[fasync::run_singlethreaded(test)]
async fn builtin_time_service_and_clock_routed() -> Result<(), Error> {
    fuchsia_syslog::init().unwrap();
    fuchsia_syslog::set_severity(fuchsia_syslog::levels::WARN);

    let expected_backstop_time = zx::Time::from_nanos(1589910459000000000);
    let expected_backstop_time_sec_str = "1589910459";

    // Construct a pseudo-directory to mock the component manager's configured
    // backstop time.
    let dir = pseudo_directory! {
        "config" => pseudo_directory! {
            "build_info" => pseudo_directory! {
                // The backstop time is stored in seconds.
                "minimum_utc_stamp" => read_only_static(expected_backstop_time_sec_str),
            },
        },
    };

    let (client, server) = zx::Channel::create().expect("failed to create channel pair");
    let scope = ExecutionScope::new();
    dir.open(
        scope,
        fio::OPEN_FLAG_DIRECTORY | fio::OPEN_RIGHT_READABLE,
        0,
        vfs::path::Path::empty(),
        ServerEnd::new(server),
    );

    // Start a component_manager as a v1 component, with the extra `--maintain-utc-clock` flag.
    debug!("starting component_manager");
    let test = OpaqueTestBuilder::new("fuchsia-pkg://fuchsia.com/utc-time-tests#meta/realm.cm")
        .component_manager_url(
            "fuchsia-pkg://fuchsia.com/utc-time-tests#meta/component_manager.cmx",
        )
        .config("/pkg/data/cm_config")
        .add_dir_handle("/boot", client.into())
        .build()
        .await
        .expect("failed to start the OpaqueTest");
    let event_source = test
        .connect_to_event_source()
        .await
        .expect("failed to connect to the EventSource protocol");

    // Inject TestOutcomeCapabilities which the components launched by component manager request
    // and use to report back the test outcome.
    // The capabilities can only be used once, so multiple components requesting this will panic.
    debug!("injecting capabilities");
    let (maintain_capability, maintain_fut) = test_outcome_report();
    maintain_capability.inject(&event_source, EventMatcher::ok().moniker("/maintainer:*")).await;

    let (client_capability, client_fut) = test_outcome_report();
    client_capability.inject(&event_source, EventMatcher::ok().moniker("/time_client:*")).await;

    // Unblock the component_manager.
    debug!("starting component tree");
    event_source.start_component_tree().await;

    // Await the test result.
    debug!("waiting for test outcome");
    let maintain_result = maintain_fut.await??;
    let client_result = client_fut.await??;

    // Check that times reported by the maintainer and client agree.
    assert_eq!(expected_backstop_time.into_nanos(), maintain_result.backstop);
    assert_eq!(expected_backstop_time.into_nanos(), client_result.backstop);
    let expected_current_time = expected_backstop_time + TEST_OFFSET;
    assert_geq!(expected_current_time.into_nanos(), maintain_result.current_time);
    assert_geq!(client_result.current_time, maintain_result.current_time);

    Ok(())
}

type TestResult = Result<ftest::SuccessOutcome, Error>;

/// The TestOutcomeCapability that can be injected in order for a component under test
/// to report back test outcomes.
pub struct TestOutcomeCapability {
    sender: Mutex<Option<oneshot::Sender<TestResult>>>,
}

/// Create a TestOutcomeCapability and Receiver pair.
fn test_outcome_report() -> (Arc<TestOutcomeCapability>, oneshot::Receiver<TestResult>) {
    let (sender, receiver) = oneshot::channel::<TestResult>();
    (Arc::new(TestOutcomeCapability { sender: Mutex::new(Some(sender)) }), receiver)
}

#[async_trait]
impl ProtocolInjector for TestOutcomeCapability {
    type Marker = ftest::TestOutcomeReportMarker;

    async fn serve(
        self: Arc<Self>,
        mut request_stream: ftest::TestOutcomeReportRequestStream,
    ) -> Result<(), Error> {
        let sender = match self.sender.lock().await.take() {
            Some(sender) => sender,
            None => panic!("TestOutcomeCapability already reported"),
        };
        sender
            .send(match request_stream.next().await {
                Some(outcome) => outcome.map_err(|e| e.into()).and_then(
                    |ftest::TestOutcomeReportRequest::Report { outcome, responder }| {
                        responder.send().context("failed to send response")?;
                        match outcome {
                            ftest::TestOutcome::Success(success) => Ok(success),
                            ftest::TestOutcome::Failed(ftest::FailedOutcome { message }) => {
                                Err(anyhow!("test failed: {}", &message))
                            }
                        }
                    },
                ),
                None => Err(anyhow!("channel finished without a response")),
            })
            .expect("failed to send outcome on channel");
        Ok(())
    }
}
