// Copyright 2018-2021 Cargill Incorporated
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Provides functionality to apply available rule arguments to create a `SplinterServiceBuilder`.

use crate::admin::messages::is_valid_service_id;
use crate::base62::next_base62_string;

use super::super::{yaml_parser::v1, CircuitTemplateError, SplinterServiceBuilder};
use super::{get_argument_value, is_arg_value, RuleArgument, Value};

const ALL_OTHER_SERVICES: &str = "$(ALL_OTHER_SERVICES)";
const NODES_ARG: &str = "NODES";
const PEER_SERVICES_ARG: &str = "peer_services";

/// Data structure used to create a `SplinterServiceBuilder`.
pub(super) struct CreateServices {
    service_type: String,
    service_args: Vec<ServiceArgument>,
    first_service: String,
}

impl CreateServices {
    /// Builds a `SplinterServiceBuilder` using the available circuit template arguments.
    pub fn apply_rule(
        &self,
        template_arguments: &[RuleArgument],
    ) -> Result<Vec<SplinterServiceBuilder>, CircuitTemplateError> {
        let nodes = get_argument_value(NODES_ARG, template_arguments)?
            .split(',')
            .map(String::from)
            .collect::<Vec<String>>();

        if !is_valid_service_id(&self.first_service) {
            return Err(CircuitTemplateError::new(&format!(
                "Field first_service is invalid (\"{}\"): must be a 4 character base62 string",
                self.first_service,
            )));
        }

        let mut service_id = self.first_service.clone();
        let mut service_builders = vec![];
        for node in nodes {
            let splinter_service_builder = SplinterServiceBuilder::new()
                .with_service_id(&service_id)
                .with_allowed_nodes(&[node])
                .with_service_type(&self.service_type);

            service_builders.push(splinter_service_builder);
            service_id = next_base62_string(&service_id)
                .map_err(|err| {
                    CircuitTemplateError::new_with_source(
                        "Failed to get next service ID",
                        err.into(),
                    )
                })?
                .ok_or_else(|| {
                    CircuitTemplateError::new("Exceeded number of services that can be built")
                })?;
        }

        let mut new_service_args = Vec::new();
        for arg in self.service_args.iter() {
            match &arg.value {
                Value::Single(value) => {
                    if arg.key == PEER_SERVICES_ARG && value == ALL_OTHER_SERVICES {
                        service_builders = all_services(service_builders)?;
                    } else {
                        let value = if is_arg_value(value) {
                            get_argument_value(value, template_arguments)?
                        } else {
                            value.clone()
                        };
                        new_service_args.push((arg.key.clone(), value));
                    }
                }
                Value::List(values) => {
                    let vals = values
                        .iter()
                        .try_fold::<_, _, Result<_, CircuitTemplateError>>(
                            Vec::new(),
                            |mut acc, value| {
                                let value = if is_arg_value(value) {
                                    get_argument_value(value, template_arguments)?
                                } else {
                                    value.to_string()
                                };
                                acc.push(format!("\"{}\"", value));
                                Ok(acc)
                            },
                        )?;
                    new_service_args.push((arg.key.clone(), format!("[{}]", vals.join(","))));
                }
            }
        }

        service_builders = service_builders
            .into_iter()
            .map(|builder| {
                let mut service_args = builder.arguments().unwrap_or_default();
                service_args.extend(new_service_args.clone());
                builder.with_arguments(&service_args)
            })
            .collect::<Vec<SplinterServiceBuilder>>();

        Ok(service_builders)
    }
}

#[derive(Debug)]
struct ServiceArgument {
    key: String,
    value: Value,
}

impl From<v1::CreateServices> for CreateServices {
    fn from(yaml_create_services: v1::CreateServices) -> Self {
        CreateServices {
            service_type: yaml_create_services.service_type().to_string(),
            service_args: yaml_create_services
                .service_args()
                .to_owned()
                .into_iter()
                .map(ServiceArgument::from)
                .collect(),
            first_service: yaml_create_services.first_service().to_string(),
        }
    }
}

impl From<v1::ServiceArgument> for ServiceArgument {
    fn from(yaml_service_argument: v1::ServiceArgument) -> Self {
        ServiceArgument {
            key: yaml_service_argument.key().to_string(),
            value: Value::from(yaml_service_argument.value().clone()),
        }
    }
}

fn all_services(
    service_builders: Vec<SplinterServiceBuilder>,
) -> Result<Vec<SplinterServiceBuilder>, CircuitTemplateError> {
    let peers = service_builders
        .iter()
        .map(|builder| {
            let service_id = builder.service_id().ok_or_else(|| {
                error!(
                    "The service_id must be set before the service argument PEER_SERVICES can \
                     be set"
                );
                CircuitTemplateError::new("Failed to parse template due to an internal error")
            })?;
            Ok(format!("\"{}\"", service_id))
        })
        .collect::<Result<Vec<String>, CircuitTemplateError>>()?;
    let services = service_builders
        .into_iter()
        .enumerate()
        .map(|(index, builder)| {
            let mut service_peers = peers.clone();
            service_peers.remove(index);
            let mut service_args = builder.arguments().unwrap_or_default();
            service_args.push((
                PEER_SERVICES_ARG.into(),
                format!("[{}]", service_peers.join(",")),
            ));
            builder.with_arguments(&service_args)
        })
        .collect::<Vec<SplinterServiceBuilder>>();
    Ok(services)
}

#[cfg(test)]
mod test {
    use super::*;

    /// Verify that a `SplinterServiceBuilder` is accurately constructed using the `CreateServices`
    /// `apply_rule` method.
    ///
    /// The test follows the procedure below:
    /// 1. Generate a `CreateServices` object and a set of template arguments using mock data.
    /// 2. Use the `apply_rule` method of the `CreateServices` object created in the previous step,
    ///    resulting in 2 `SplinterServiceBuilder` objects.
    ///
    /// The `SplinterServiceBuilder` objects are then verified to have all expected values.
    #[test]
    fn test_create_service_apply_rules() {
        let create_services = make_create_service();
        let template_arguments = make_rule_arguments();

        let service_builders = create_services
            .apply_rule(&template_arguments)
            .expect("Failed to apply rules");

        assert_eq!(service_builders.len(), 2);

        assert_eq!(
            service_builders[0].allowed_nodes(),
            Some(vec!["alpha-node-000".to_string()])
        );
        assert_eq!(service_builders[0].service_id(), Some("a000".to_string()));
        assert_eq!(
            service_builders[0].service_type(),
            Some("scabbard".to_string())
        );

        let service_args = service_builders[0]
            .arguments()
            .expect("Services args were not set");
        assert_eq!(service_args.len(), 2);
        assert_eq!(
            service_args[0],
            (PEER_SERVICES_ARG.to_string(), "[\"a001\"]".to_string())
        );
        assert_eq!(
            service_args[1],
            ("admin-keys".to_string(), "[\"signer_key\"]".to_string())
        );

        assert_eq!(
            service_builders[1].allowed_nodes(),
            Some(vec!["beta-node-000".to_string()])
        );
        assert_eq!(service_builders[1].service_id(), Some("a001".to_string()));
        assert_eq!(
            service_builders[1].service_type(),
            Some("scabbard".to_string())
        );

        let service_args = service_builders[1]
            .arguments()
            .expect("Services args were not set");
        assert_eq!(service_args.len(), 2);
        assert_eq!(
            service_args[0],
            (PEER_SERVICES_ARG.to_string(), "[\"a000\"]".to_string())
        );
        assert_eq!(
            service_args[1],
            ("admin-keys".to_string(), "[\"signer_key\"]".to_string())
        );

        // Verify each `SplinterServiceBuilder` is able to `build` successfully.
        assert!(service_builders[0].clone().build().is_ok());
        assert!(service_builders[1].clone().build().is_ok());
    }

    /// Verify the `CreateServices` `apply_rule` method accurately detects an invalid
    /// `first_service`. In order to test the breadth of possible invalidities, this test creates
    /// multiple, different invalid `first_service` objects.
    /// Before the `first_service` objects are tested, a set of template arguments are generated
    /// using mock data, to be passed to the `apply_rule` method for each invalid case.
    ///
    /// The different invalidities are tested as follows:
    ///
    /// 1. Once a `CreateServices` object has been created, the `first_service` field is set to
    ///    an empty string. Then verifies the `apply_rule` method returns an error.
    ///
    /// 2. Once a `CreateServices` object has been created, the `first_service` field is set to
    ///    a 3-character string, 'a00', which is invalid as this field must be a 4-character base-62
    ///    string. Then verifies the `apply_rule` method returns an error.
    ///
    /// 3. Once a `CreateServices` object has been created, the `first_service` field is set to
    ///    a 5-character string, 'a0000', which is invalid for the same reason as the previous step.
    ///    Then verifies the `apply_rule` method returns an error.
    ///
    /// 4. Once a `CreateServices` object has been created, the `first_service` field is set to a
    ///    4-character string, with an invalid character, ':'. This character is invalid as the
    ///   field must contain a base-62 string. Then verifies the `apply_rule` method returns an error.
    ///
    #[test]
    fn test_create_service_apply_rules_invalid_first_service() {
        let template_arguments = make_rule_arguments();

        let mut empty = make_create_service();
        empty.first_service = "".to_string();
        assert!(empty.apply_rule(&template_arguments).is_err());

        let mut too_short = make_create_service();
        too_short.first_service = "a00".to_string();
        assert!(too_short.apply_rule(&template_arguments).is_err());

        let mut too_long = make_create_service();
        too_long.first_service = "a0000".to_string();
        assert!(too_long.apply_rule(&template_arguments).is_err());

        let mut invalid_char = make_create_service();
        invalid_char.first_service = "a0:0".to_string();
        assert!(invalid_char.apply_rule(&template_arguments).is_err());
    }

    fn make_create_service() -> CreateServices {
        let peer_services_arg = ServiceArgument {
            key: PEER_SERVICES_ARG.to_string(),
            value: Value::Single(ALL_OTHER_SERVICES.to_string()),
        };
        let admin_keys_arg = ServiceArgument {
            key: "admin-keys".to_string(),
            value: Value::List(vec!["$(ADMIN_KEYS)".to_string()]),
        };

        CreateServices {
            service_type: "scabbard".to_string(),
            service_args: vec![peer_services_arg, admin_keys_arg],
            first_service: "a000".to_string(),
        }
    }

    fn make_rule_arguments() -> Vec<RuleArgument> {
        let admin_keys_template_arg = RuleArgument {
            name: "admin_keys".to_string(),
            required: false,
            default_value: Some("$(SIGNER_PUB_KEY)".to_string()),
            description: None,
            user_value: None,
        };

        let nodes_template_arg = RuleArgument {
            name: "nodes".to_string(),
            required: true,
            default_value: None,
            description: None,
            user_value: Some("alpha-node-000,beta-node-000".to_string()),
        };

        let signer_pub_key = RuleArgument {
            name: "signer_pub_key".to_string(),
            required: false,
            default_value: None,
            description: None,
            user_value: Some("signer_key".to_string()),
        };

        vec![admin_keys_template_arg, nodes_template_arg, signer_pub_key]
    }
}
