Documentation
use crate::effect::Effect;
use crate::request::Request;
use crate::statement::Statement;
use ic_stable_structures::Storable;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;

#[derive(Serialize, Deserialize, Default)]
pub struct Policy {
    statements: Vec<Statement>,
}

impl Policy {
    fn get_effect(&self, request: &Request) -> Effect {
        let mut effects = vec![];
        for statement in &self.statements {
            match statement.get_effect(request) {
                None => {}
                Some(effect) => effects.push(effect),
            }
        }
        effects.sort();
        effects.get(0).cloned().unwrap_or(Effect::Deny)
    }

    pub fn add_statement(&mut self, statement: Statement) -> () {
        self.statements.push(statement);
    }
}

pub fn get_effect(p: &Policy, r: &Request) -> Effect {
    return p.get_effect(r);
}

impl Storable for Policy {
    fn to_bytes(&self) -> Cow<[u8]> {
        match serde_json::to_vec(&self) {
            Ok(result) => {
                return Cow::from(result.as_slice().to_owned()).to_owned();
            }
            Err(_) => {
                panic!("Failed to serialize!")
            }
        }
    }

    fn from_bytes(bytes: Cow<[u8]>) -> Self {
        match serde_json::from_slice::<Self>(&*bytes) {
            Ok(result) => result,
            Err(_) => {
                panic!("Failed to deserialize!")
            }
        }
    }
}

#[cfg(test)]
mod policy_tests {
    use crate::effect::Effect;
    use crate::policy::Policy;
    use crate::request::RequestResourceBuilder;
    use crate::statement::{Identity, Statement, StatementIdentity, StatementResource};
    use candid::Principal;

    use super::*;

    #[test]
    pub fn it_matches_policy() {
        let policy = Policy {
            statements: vec![Statement::new(
                Effect::Allow,
                vec![StatementIdentity::Identity(Identity::Principal(
                    Principal::anonymous(),
                ))],
                vec!["call".to_string()],
                vec![StatementResource::Resource("Foo".to_string())],
            )],
        };

        assert!(
            policy.get_effect(&Request::new(
                "call".to_string(),
                RequestResourceBuilder::new("Foo").build(),
                Principal::anonymous()
            )) == Effect::Allow
        );

        assert!(
            policy.get_effect(&Request::new(
                "call".to_string(),
                RequestResourceBuilder::new("Bar").build(),
                Principal::anonymous()
            )) == Effect::Deny
        );
    }

    #[test]
    pub fn it_selects_least_permissive() {
        let policy = Policy {
            statements: vec![
                Statement::new(
                    Effect::Deny,
                    vec![StatementIdentity::Identity(Identity::Principal(
                        Principal::anonymous(),
                    ))],
                    vec!["call".to_string()],
                    vec![StatementResource::Resource("Foo".to_string())],
                ),
                Statement::new(
                    Effect::Allow,
                    vec![StatementIdentity::Identity(Identity::Principal(
                        Principal::anonymous(),
                    ))],
                    vec!["call".to_string()],
                    vec![StatementResource::Resource("Foo".to_string())],
                ),
                Statement::new(
                    Effect::Allow,
                    vec![StatementIdentity::Identity(Identity::Principal(
                        Principal::anonymous(),
                    ))],
                    vec!["call".to_string()],
                    vec![StatementResource::Resource("Foo".to_string())
                        .add_nested(StatementResource::Resource("Bar".to_string()))],
                ),
            ],
        };

        assert!(
            policy.get_effect(&Request::new(
                "call".to_string(),
                RequestResourceBuilder::new("Foo").build(),
                Principal::anonymous()
            )) == Effect::Deny
        );

        assert!(
            policy.get_effect(&Request::new(
                "call".to_string(),
                RequestResourceBuilder::new("Foo").add("Bar").build(),
                Principal::anonymous()
            )) == Effect::Allow
        );
    }
}