/*
 * Copyright Cedar Contributors
 *
 * 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
 *
 *      https://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.
 */

//! Contains test for typechecking with partial schema files.
#![cfg(test)]
#![cfg(feature = "partial-validate")]
// GRCOV_STOP_COVERAGE

use std::collections::HashSet;

use cedar_policy_core::ast::{Expr, Template, Var};
use cedar_policy_core::{ast::StaticPolicy, parser::parse_policy};

use crate::typecheck::test_utils::{assert_expected_type_errors, assert_expected_warnings};
use crate::typecheck::Typechecker;
use crate::types::{EntityLUB, Type};
use crate::UnexpectedTypeHelp;
use crate::{
    AttributeAccess, NamespaceDefinition, TypeError, ValidationMode, ValidationWarning,
    ValidationWarningKind, ValidatorSchema,
};

use super::test_utils::empty_schema_file;

#[track_caller] // report the caller's location as the location of the panic, not the location in this function
pub(crate) fn assert_partial_typecheck(
    schema: impl TryInto<ValidatorSchema, Error = impl core::fmt::Debug>,
    policy: StaticPolicy,
) {
    let schema = schema.try_into().expect("Failed to construct schema.");
    let typechecker = Typechecker::new(&schema, ValidationMode::Partial);
    let mut type_errors: HashSet<TypeError> = HashSet::new();
    let mut warnings: HashSet<ValidationWarning> = HashSet::new();
    let typechecked = typechecker.typecheck_policy(
        &Template::link_static_policy(policy.clone()).0,
        &mut type_errors,
        &mut warnings,
    );
    assert_eq!(type_errors, HashSet::new(), "Did not expect any errors.");
    assert!(typechecked, "Expected that policy would typecheck.");
}

#[track_caller] // report the caller's location as the location of the panic, not the location in this function
pub(crate) fn assert_partial_typecheck_fails(
    schema: impl TryInto<ValidatorSchema, Error = impl core::fmt::Debug>,
    policy: StaticPolicy,
    expected_type_errors: Vec<TypeError>,
) {
    let schema = schema.try_into().expect("Failed to construct schema.");
    let typechecker = Typechecker::new(&schema, ValidationMode::Partial);
    let mut type_errors: HashSet<TypeError> = HashSet::new();
    let mut warnings: HashSet<ValidationWarning> = HashSet::new();
    let typechecked = typechecker.typecheck_policy(
        &Template::link_static_policy(policy.clone()).0,
        &mut type_errors,
        &mut warnings,
    );
    assert_expected_type_errors(&expected_type_errors, &type_errors);
    assert!(!typechecked, "Expected that policy would not typecheck.");
}

#[track_caller] // report the caller's location as the location of the panic, not the location in this function
pub(crate) fn assert_partial_typecheck_warns(
    schema: impl TryInto<ValidatorSchema, Error = impl core::fmt::Debug>,
    policy: StaticPolicy,
    expected_warnings: Vec<ValidationWarningKind>,
) {
    let schema = schema.try_into().expect("Failed to construct schema.");
    let typechecker = Typechecker::new(&schema, ValidationMode::Partial);
    let mut type_errors: HashSet<TypeError> = HashSet::new();
    let mut warnings: HashSet<ValidationWarning> = HashSet::new();
    let static_policy = Template::link_static_policy(policy.clone());
    let typechecked =
        typechecker.typecheck_policy(&static_policy.0, &mut type_errors, &mut warnings);
    assert_expected_warnings(&expected_warnings, &warnings);
    assert!(
        typechecked,
        "Expected that policy would typecheck (with warnings)."
    );
}

#[track_caller] // report the caller's location as the location of the panic, not the location in this function
pub(crate) fn assert_typechecks_empty_schema(policy: StaticPolicy) {
    assert_partial_typecheck(empty_schema_file(), policy)
}

#[track_caller] // report the caller's location as the location of the panic, not the location in this function
pub(crate) fn assert_typecheck_fails_empty_schema(
    policy: StaticPolicy,
    expected_type_errors: Vec<TypeError>,
) {
    assert_partial_typecheck_fails(empty_schema_file(), policy, expected_type_errors)
}

#[track_caller] // report the caller's location as the location of the panic, not the location in this function
pub(crate) fn assert_typecheck_warns_empty_schema(
    policy: StaticPolicy,
    expected_warnings: Vec<ValidationWarningKind>,
) {
    assert_partial_typecheck_warns(empty_schema_file(), policy, expected_warnings)
}

mod passes_empty_schema {

    use super::*;

    #[test]
    fn principal_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { principal.is_admin };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn action_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) unless { action.is_restricted };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn resource_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { resource.is_public };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn literal_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { User::"alice".is_admin };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn get_nested_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { resource.foo.bar.baz.buz };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn principal_has_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { principal has is_admin };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn action_has_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) unless { action has is_restricted };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn resource_has_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { resource has is_public };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn literal_has_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { User::"alice" has is_admin };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn has_nested_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { resource.foo has bar };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn principal_eq() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal == User::"alice", action, resource);"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn action_eq() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action == Action::"view", resource);"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn resource_eq() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource == Photo::"vacation.jpg");"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn principal_in() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal in User::"alice", action, resource);"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn action_in() {
        // Test case with `in` are  interesting because, for an undeclared
        // entity type or action we don't know what should be `in` it, so the
        // expression has type boolean (not known to be true or false).
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action in Action::"view", resource);"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn resource_in() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource in Photo::"vacation.jpg");"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn principal_in_set() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when {  principal in [User::"alice", Admin::"bob"] };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn action_in_set() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action in [Action::"view", Action::"edit"], resource);"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn in_attr() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { principal in resource.owner };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn attr_in() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { resource.owner in principal };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn resource_in_set() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { resource in [Photo::"vacation.jpg", Album::"vacation_photos"] } ;"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn if_both_branches_undefined() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { if principal.foo then action.bar else resource.bar };"#
            )
            .unwrap(),
        );
    }

    #[test]
    fn if_one_branch_undefined() {
        assert_typechecks_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { 0 < (if principal.foo then action.bar else 1) };"#
            )
            .unwrap(),
        );
    }

    #[test]
    fn context_attr() {
        let p = parse_policy(
            Some("0".to_string()),
            r#"permit(principal, action, resource) when { context.foo };"#,
        )
        .expect("Policy should parse.");
        assert_typechecks_partial_schema(p);
        let p = parse_policy(
            Some("0".to_string()),
            r#"permit(principal, action, resource) when { context.foo.bar };"#,
        )
        .expect("Policy should parse.");
        assert_typechecks_partial_schema(p);
    }
}

mod fails_empty_schema {
    use std::str::FromStr;

    use cedar_policy_core::ast::Expr;

    use crate::types::Type;

    use super::*;

    #[test]
    fn operator_type_error() {
        // We expect to see a type error for the incorrect literal argument to
        // various operators. No error should be generated for missing
        // attributes or the type of the attributes.
        assert_typecheck_fails_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { principal.foo > "a" };"#,
            )
            .unwrap(),
            vec![TypeError::expected_type(
                Expr::val("a"),
                Type::primitive_long(),
                Type::primitive_string(),
                None,
            )],
        );
        assert_typecheck_fails_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { 1.contains(principal.foo) };"#,
            )
            .unwrap(),
            vec![TypeError::expected_type(
                Expr::val(1),
                Type::any_set(),
                Type::primitive_long(),
                None,
            )],
        );
        assert_typecheck_fails_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { principal.foo.containsAll(1) };"#,
            )
            .unwrap(),
            vec![TypeError::expected_type(
                Expr::val(1),
                Type::any_set(),
                Type::primitive_long(),
                Some(UnexpectedTypeHelp::TryUsingSingleContains),
            )],
        );
    }

    #[test]
    fn top_level_type_error() {
        assert_typecheck_fails_empty_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { principal.foo + 1 };"#,
            )
            .unwrap(),
            vec![TypeError::expected_type(
                Expr::from_str("principal.foo + 1").unwrap(),
                Type::primitive_boolean(),
                Type::primitive_long(),
                None,
            )],
        )
    }

    #[test]
    fn impossible_policy() {
        let p = parse_policy(
            None,
            r#"permit(principal, action, resource) when { resource.bar && false };"#,
        )
        .unwrap();
        assert_typecheck_warns_empty_schema(
            p.clone(),
            vec![ValidationWarningKind::ImpossiblePolicy],
        )
    }

    #[test]
    fn record_lit_bad_attr() {
        let p = parse_policy(
            None,
            r#"permit(principal, action, resource) when { {foo: 1}.bar };"#,
        )
        .unwrap();
        assert_typecheck_fails_empty_schema(
            p.clone(),
            vec![TypeError::unsafe_attribute_access(
                Expr::from_str("{foo: 1}.bar").unwrap(),
                AttributeAccess::Other(vec!["bar".into()]),
                Some("foo".into()),
                false,
            )],
        )
    }
}

fn partial_schema_file() -> NamespaceDefinition {
    serde_json::from_value(serde_json::json!(
        {
            "entityTypes": {
                "User": {
                    "memberOfTypes": [ "Group" ],
                    "shape": {
                        "type": "Record",
                        "additionalAttributes": true,
                        "attributes": {
                            "name": { "type": "String", "required": true},
                            "age": { "type": "Long", "required": true},
                            "favorite": { "type": "Entity", "name": "Photo", "required": true}
                        }
                    }
                },
                "Group": {
                    "shape": {
                        "type": "Record",
                        "additionalAttributes": false,
                        "attributes": {}
                    }
                },
                "Photo": {
                    "shape": {
                        "type": "Record",
                        "additionalAttributes": true,
                        "attributes": {}
                    }
                }
            },
            "actions": {
                "view_photo": {
                    "appliesTo": {
                        "principalTypes": ["User", "Group"],
                        "resourceTypes": ["Photo"],
                        "context": {
                            "type": "Record",
                            "additionalAttributes": true,
                            "attributes": {}
                        }
                    }
                }
            }
        }
    ))
    .expect("Expected valid schema")
}

#[track_caller] // report the caller's location as the location of the panic, not the location in this function
pub(crate) fn assert_typechecks_partial_schema(policy: StaticPolicy) {
    assert_partial_typecheck(partial_schema_file(), policy)
}

#[track_caller] // report the caller's location as the location of the panic, not the location in this function
pub(crate) fn assert_typecheck_fails_partial_schema(
    policy: StaticPolicy,
    expected_type_errors: Vec<TypeError>,
) {
    assert_partial_typecheck_fails(partial_schema_file(), policy, expected_type_errors)
}

mod passes_partial_schema {
    use super::*;

    #[test]
    fn unknown_attr_on_declared_entity_type() {
        assert_typechecks_partial_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { User::"alice".unknown };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn has_unknown_attr_on_declared_entity_type() {
        assert_typechecks_partial_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { User::"alice" has unknown };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn undeclared_entity_type_in_declared_entity_type() {
        assert_typechecks_partial_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { Admin::"alice" in User::"alice" };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn undeclared_action_in_declared_action() {
        assert_typechecks_partial_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { Action::"undeclared" in Action::"view" };"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn principal_wrong_for_known_actions() {
        // `resource` can't be a `Group` for the defined actions, but there
        // might be an undefined action where that's OK.
        assert_typechecks_partial_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource == Group::"owners");"#,
            )
            .unwrap(),
        );
    }

    #[test]
    fn policy_in_set_action_and_other() {
        let p = parse_policy(
            Some("0".to_string()),
            r#"permit(principal, action, resource) when { User::"alice" in [action, User::"alice"] };"#,
        ).expect("Policy should parse.");
        assert_typechecks_partial_schema(p);
    }

    #[test]
    fn policy_action_in_set_action_and_other() {
        let p = parse_policy(
            Some("0".to_string()),
            r#"permit(principal, action, resource) when { action in [action, User::"alice"] };"#,
        )
        .expect("Policy should parse.");
        assert_typechecks_partial_schema(p);
    }

    #[test]
    fn context_attr() {
        let p = parse_policy(
            Some("0".to_string()),
            r#"permit(principal, action == Action::"view_photo", resource) when { context.foo };"#,
        )
        .expect("Policy should parse.");
        assert_typechecks_partial_schema(p);
    }
}

mod fail_partial_schema {

    use std::str::FromStr;

    use super::*;
    use crate::{LubContext, LubHelp};

    #[test]
    fn error_on_declared_attr() {
        // `name` is declared as a `String` in the partial schema, so we can
        // error even though `principal.unknown` is not declared.
        assert_typecheck_fails_partial_schema(
            parse_policy(
                None,
                r#"permit(principal == User::"alice", action, resource) when { principal.name > principal.unknown };"#,
            )
            .unwrap(),
            vec![TypeError::expected_type(
                Expr::get_attr(Expr::var(Var::Principal), "name".into()),
                Type::primitive_long(),
                Type::primitive_string(),
                None,
            )],
        );
    }

    #[test]
    fn incompatible_attrs() {
        assert_typecheck_fails_partial_schema(
            // `age` and `name` are defined with incompatible types, while
            // `unknown` is not defined. The conflict is noticed and an error is
            // raised.
            parse_policy(
                None,
                r#"permit(principal == User::"alice", action, resource) when {
                        (if resource.foo then
                            principal.age
                        else (if resource.bar then
                            principal.name
                        else
                            principal.unknown
                        )) == "alice"};"#,
            )
            .unwrap(),
            vec![TypeError::incompatible_types(
                Expr::from_str("if resource.foo then principal.age else (if resource.bar then principal.name else principal.unknown)").unwrap(),
                vec![Type::primitive_long(), Type::primitive_string()],
                LubHelp::None,
                LubContext::Conditional,
            )],
        );
    }

    #[test]
    fn unknown_attr_on_closed_entity_type() {
        assert_typecheck_fails_partial_schema(
            parse_policy(
                None,
                r#"permit(principal, action, resource) when { principal.is_foo };"#,
            )
            .unwrap(),
            vec![TypeError::unsafe_attribute_access(
                Expr::from_str("principal.is_foo").unwrap(),
                AttributeAccess::EntityLUB(
                    EntityLUB::single_entity("Group".parse().unwrap()),
                    vec!["is_foo".into()],
                ),
                None,
                false,
            )],
        );
    }
}
