require "helper"
require "inspec/resource"
require "resources/aws/aws_iam_policy"

require "resource_support/aws"
require "resources/aws/aws_iam_policy"

# MAIPSB = MockAwsIamPolicySingularBackend
# Abbreviation not used outside this file

#=============================================================================#
#                            Constructor Tests
#=============================================================================#
class AwsIamPolicyConstructorTest < Minitest::Test

  def setup
    AwsIamPolicy::BackendFactory.select(MAIPSB::Empty)
  end

  def test_rejects_empty_params
    assert_raises(ArgumentError) { AwsIamPolicy.new }
  end

  def test_accepts_policy_name_as_scalar
    AwsIamPolicy.new("test-policy-1")
  end

  def test_accepts_policy_name_as_hash
    AwsIamPolicy.new(policy_name: "test-policy-1")
  end

  def test_rejects_unrecognized_params
    assert_raises(ArgumentError) { AwsIamPolicy.new(shoe_size: 9) }
  end
end

#=============================================================================#
#                               Search / Recall
#=============================================================================#
class AwsIamPolicyRecallTest < Minitest::Test

  def setup
    AwsIamPolicy::BackendFactory.select(MAIPSB::Basic)
  end

  def test_search_hit_via_scalar_works
    assert AwsIamPolicy.new("test-policy-1").exists?
  end

  def test_search_hit_via_hash_works
    assert AwsIamPolicy.new(policy_name: "test-policy-1").exists?
  end

  def test_search_miss_is_not_an_exception
    refute AwsIamPolicy.new(policy_name: "non-existant").exists?
  end
end

#=============================================================================#
#                               Properties
#=============================================================================#
class AwsIamPolicyPropertiesTest < Minitest::Test

  def setup
    AwsIamPolicy::BackendFactory.select(MAIPSB::Basic)
  end

  def test_property_arn
    assert_equal("arn:aws:iam::aws:policy/test-policy-1", AwsIamPolicy.new("test-policy-1").arn)
    assert_nil(AwsIamPolicy.new(policy_name: "non-existant").arn)
  end

  def test_property_default_version_id
    assert_equal("v1", AwsIamPolicy.new("test-policy-1").default_version_id)
    assert_nil(AwsIamPolicy.new(policy_name: "non-existant").default_version_id)
  end

  def test_property_attachment_count
    assert_equal(3, AwsIamPolicy.new("test-policy-1").attachment_count)
    assert_nil(AwsIamPolicy.new(policy_name: "non-existant").attachment_count)
  end

  def test_property_attached_users
    assert_equal(["test-user"], AwsIamPolicy.new("test-policy-1").attached_users)
    assert_nil(AwsIamPolicy.new(policy_name: "non-existant").attached_users)
  end

  def test_property_attached_groups
    assert_equal(["test-group"], AwsIamPolicy.new("test-policy-1").attached_groups)
    assert_nil(AwsIamPolicy.new(policy_name: "non-existant").attached_groups)
  end

  def test_property_attached_roles
    assert_equal(["test-role"], AwsIamPolicy.new("test-policy-1").attached_roles)
    assert_nil(AwsIamPolicy.new(policy_name: "non-existant").attached_roles)
  end

  def test_property_policy
    policy = AwsIamPolicy.new("test-policy-1").policy
    assert_kind_of(Hash, policy)
    assert(policy.key?("Statement"), "test-policy-1 should have a Statement key when unpacked")
    assert_equal(1, policy["Statement"].count, "test-policy-1 should have 1 statements when unpacked")
    assert_nil(AwsIamPolicy.new("non-existant").policy)
  end

  def test_property_statement_count
    assert_nil(AwsIamPolicy.new("non-existant").statement_count)
    assert_equal(1, AwsIamPolicy.new("test-policy-1").statement_count)
    assert_equal(2, AwsIamPolicy.new("test-policy-2").statement_count)
    assert_equal(1, AwsIamPolicy.new("test-policy-3").statement_count)
  end
end

#=============================================================================#
#                               Matchers
#=============================================================================#
class AwsIamPolicyMatchersTest < Minitest::Test

  def setup
    AwsIamPolicy::BackendFactory.select(MAIPSB::Basic)
  end

  def test_matcher_attached_positive
    assert AwsIamPolicy.new("test-policy-1").attached?
  end

  def test_matcher_attached_negative
    refute AwsIamPolicy.new("test-policy-2").attached?
  end

  def test_matcher_attached_to_user_positive
    assert AwsIamPolicy.new("test-policy-1").attached_to_user?("test-user")
  end

  def test_matcher_attached_to_user_negative
    refute AwsIamPolicy.new("test-policy-2").attached_to_user?("test-user")
  end

  def test_matcher_attached_to_group_positive
    assert AwsIamPolicy.new("test-policy-1").attached_to_group?("test-group")
  end

  def test_matcher_attached_to_group_negative
    refute AwsIamPolicy.new("test-policy-2").attached_to_group?("test-group")
  end

  def test_matcher_attached_to_role_positive
    assert AwsIamPolicy.new("test-policy-1").attached_to_role?("test-role")
  end

  def test_matcher_attached_to_role_negative
    refute AwsIamPolicy.new("test-policy-2").attached_to_role?("test-role")
  end

  def test_have_statement_when_policy_does_not_exist
    assert_nil AwsIamPolicy.new("nonesuch").has_statement?("Effect" => "foo")
  end

  def test_have_statement_when_provided_no_criteria
    AwsIamPolicy.new("test-policy-1").has_statement?
  end

  def test_have_statement_when_provided_acceptable_criteria
    {
      "Action" => "dummy",
      "Effect" => "Deny", # This has restictions on the value provided
      "Resource" => "dummy",
      "Sid" => "dummy",
    }.each do |criterion, test_value|
      [
        criterion,
        criterion.downcase,
        criterion.to_sym,
        criterion.downcase.to_sym
      ].each do |variant|
        AwsIamPolicy.new("test-policy-1").has_statement?(variant => test_value)
      end
    end
  end

  def test_have_statement_when_provided_unimplemented_criteria
    %w{
      Conditional
      NotAction
      NotPrincipal
      NotResource
      Principal
    }.each do |criterion|
      ex = assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(criterion => "dummy") }
      assert_match(/not supported/, ex.message)
    end
  end

  def test_have_statement_when_provided_unrecognized_criteria
    ex = assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?("foo" => "dummy") }
    assert_match(/Unrecognized/, ex.message)
  end

  def test_have_statement_when_sid_is_provided
    ["Sid", "sid", :Sid, :sid].each do |variant|
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "beta01"))
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "CloudWatchEventsFullAccess"))
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "IAMPassRoleForCloudWatchEvents"))
      refute(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "beta01"))

      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => /eta/))
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => /CloudWatch/))
      refute(AwsIamPolicy.new("test-policy-2").has_statement?(variant => /eta/))
    end
  end

  def test_have_statement_when_effect_is_provided
    ["Effect", "effect", :Effect, :effect].each do |variant|
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "Deny"))
      refute(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "Allow"))
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "Allow"))

      assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(variant => "Disallow") }
      assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(variant => "allow") }
      assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(variant => :Allow) }
      assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(variant => :allow) }
    end
  end

  def test_have_statement_when_action_is_provided
    ["Action", "action", :Action, :action].each do |variant|
      # Able to match a simple string action when multiple statements present
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "iam:PassRole"))
      # Able to match a wildcard string action
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "events:*"))
      # Do not match a wildcard when using strings
      refute(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "events:EnableRule"))
      # Do match when using a regex
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => /^events\:/))
      # Able to match one action when the statement has an array of actions
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "ec2:DescribeSubnets"))
      # Do not match if only one action specified as an array when the statement has an array of actions
      refute(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["ec2:DescribeSubnets"]))
      # Do match if two actions specified when the statement has an array of actions
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["ec2:DescribeSubnets", "ec2:DescribeSecurityGroups"]))
      # Do match setwise if two actions specified when the statement has an array of actions
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["ec2:DescribeSecurityGroups", "ec2:DescribeSubnets"]))
      # Do match if only one regex action specified when the statement has an array of actions
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => /^ec2\:Describe/))
      # Do match if one regex action specified in an array when the statement has an array of actions
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => [/^ec2\:Describe/]))
      # Able to match a degenerate policy doc in which there is exactly one statement as a hash.
      assert(AwsIamPolicy.new("test-policy-3").has_statement?(variant => "acm:GetCertificate"))
      # Don't explode, and also don't match, if a policy has a statement without an Action
      refute(AwsIamPolicy.new("test-policy-4").has_statement?(variant => "iam:*"))
    end
  end

  def test_have_statement_when_resource_is_provided
    ["Resource", "resource", :Resource, :resource].each do |variant|
      # Able to match a simple string resource when multiple statements present
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "arn:aws:iam::*:role/AWS_Events_Invoke_Targets"))
      # Able to match a wildcard string resource
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "*"))
      # Do not match a wildcard when using strings
      refute(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "arn:aws:events:us-east-1:123456789012:rule/my-rule"))
      # Do match when using a regex
      assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => /AWS_Events_Invoke_Targets$/))
      # Able to match one resource when the statement has an array of resources
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "arn:aws:ec2:::*"))
      # Do not match if only one resource specified as an array when the statement has an array of resources
      refute(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["arn:aws:ec2:::*"]))
      # Do match if two resources specified when the statement has an array of resources
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["arn:aws:ec2:::*", "*"]))
      # Do match setwise if two resources specified when the statement has an array of resources
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["*", "arn:aws:ec2:::*"]))
      # Do match if only one regex resource specified when the statement has an array of resources
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => /^arn\:aws\:ec2/))
      # Do match if one regex resource specified in an array when the statement has an array of resources
      assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => [/\*/]))
      # Able to match a degenerate policy doc in which there is exactly one statement as a hash.
      assert(AwsIamPolicy.new("test-policy-3").has_statement?(variant => "*"))
    end
  end
end

#=============================================================================#
#                               Test Fixtures
#=============================================================================#
module MAIPSB
  class Empty < AwsBackendBase
    def list_policies(query)
      OpenStruct.new(policies: [])
    end
  end

  class Basic < AwsBackendBase
    def list_policies(query)
      fixtures = [
        OpenStruct.new({
          policy_name: "test-policy-1",
          arn: "arn:aws:iam::aws:policy/test-policy-1",
          default_version_id: "v1",
          attachment_count: 3,
          is_attachable: true,
        }),
        OpenStruct.new({
          policy_name: "test-policy-2",
          arn: "arn:aws:iam::aws:policy/test-policy-2",
          default_version_id: "v1",
          attachment_count: 0,
          is_attachable: false,
        }),
        OpenStruct.new({
          policy_name: "test-policy-3",
          arn: "arn:aws:iam::aws:policy/test-policy-3",
          default_version_id: "v1",
          attachment_count: 0,
          is_attachable: true,
        }),
        OpenStruct.new({
          policy_name: "test-policy-4",
          arn: "arn:aws:iam::aws:policy/test-policy-4",
          default_version_id: "v1",
          attachment_count: 0,
          is_attachable: false,
        }),
      ]
      OpenStruct.new({ policies: fixtures })
    end

    def list_entities_for_policy(query)
      policy = {}
      policy["arn:aws:iam::aws:policy/test-policy-1"] =
        {
          policy_groups: [
            OpenStruct.new({
              group_name: "test-group",
              group_id: "AIDAIJ3FUBXLZ4VXV34LE",
            }),
          ],
          policy_users: [
            OpenStruct.new({
              user_name: "test-user",
              user_id: "AIDAIJ3FUBXLZ4VXV34LE",
            }),
          ],
          policy_roles: [
            OpenStruct.new({
              role_name: "test-role",
              role_id: "AIDAIJ3FUBXLZ4VXV34LE",
            }),
          ],
        }
      policy["arn:aws:iam::aws:policy/test-policy-2"] =
        {
          policy_groups: [],
          policy_users: [],
          policy_roles: [],
        }
      OpenStruct.new( policy[query[:policy_arn]] )
    end

    def get_policy_version(query)
      fixtures = {
        "arn:aws:iam::aws:policy/test-policy-1" => {
          "v1" => OpenStruct.new(
            # This is the integration test fixture "beta"
            # {
            #   "Version"=>"2012-10-17",
            #   "Statement"=> [
            #     {
            #       "Sid"=>"beta01",
            #       "Action"=>["ec2:DescribeSubnets", "ec2:DescribeSecurityGroups"],
            #       "Effect"=>"Deny",
            #       "Resource"=>["arn:aws:ec2:::*", "*"]
            #     }
            #   ]
            # }
            document: "%7B%0A%20%20%22Version%22%3A%20%222012-10-17%22%2C%0A%20%20%22Statement%22%3A%20%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22Sid%22%3A%20%22beta01%22%2C%0A%20%20%20%20%20%20%22Action%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%22ec2%3ADescribeSubnets%22%2C%0A%20%20%20%20%20%20%20%20%22ec2%3ADescribeSecurityGroups%22%0A%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%22Effect%22%3A%20%22Deny%22%2C%0A%20%20%20%20%20%20%22Resource%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%22arn%3Aaws%3Aec2%3A%3A%3A%2A%22%2C%0A%20%20%20%20%20%20%20%20%22%2A%22%0A%20%20%20%20%20%20%5D%0A%20%20%20%20%7D%0A%20%20%5D%0A%7D%0A"
          ),
        },
        "arn:aws:iam::aws:policy/test-policy-2" => {
          "v1" => OpenStruct.new(
            # This is AWS-managed CloudWatchEventsFullAccess
            # {
            #   "Version"=>"2012-10-17",
            #   "Statement"=> [
            #     {
            #       "Sid"=>"CloudWatchEventsFullAccess",
            #       "Effect"=>"Allow",
            #       "Action"=>"events:*",
            #       "Resource"=>"*"
            #     },
            #     {
            #       "Sid"=>"IAMPassRoleForCloudWatchEvents",
            #       "Effect"=>"Allow",
            #       "Action"=>"iam:PassRole",
            #       "Resource"=>"arn:aws:iam::*:role/AWS_Events_Invoke_Targets"
            #     }
            #   ]
            # }
            document: "%7B%0A%20%20%20%20%22Version%22%3A%20%222012-10-17%22%2C%0A%20%20%20%20%22Statement%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Sid%22%3A%20%22CloudWatchEventsFullAccess%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Action%22%3A%20%22events%3A%2A%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Resource%22%3A%20%22%2A%22%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Sid%22%3A%20%22IAMPassRoleForCloudWatchEvents%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Action%22%3A%20%22iam%3APassRole%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Resource%22%3A%20%22arn%3Aaws%3Aiam%3A%3A%2A%3Arole%2FAWS_Events_Invoke_Targets%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%5D%0A%7D"
          ),
        },
        "arn:aws:iam::aws:policy/test-policy-3" => {
          "v1" => OpenStruct.new(
            # This is AWS-managed AWSCertificateManagerReadOnly
            #   {
            #     "Version": "2012-10-17",
            #     "Statement": {
            #         "Effect": "Allow",
            #         "Action": [
            #             "acm:DescribeCertificate",
            #             "acm:ListCertificates",
            #             "acm:GetCertificate",
            #             "acm:ListTagsForCertificate"
            #         ],
            #         "Resource": "*"
            #     }
            # }
            document: "%7B%0A%20%20%20%20%22Version%22%3A%20%222012-10-17%22%2C%0A%20%20%20%20%22Statement%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%20%20%22Action%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22acm%3ADescribeCertificate%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22acm%3AListCertificates%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22acm%3AGetCertificate%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22acm%3AListTagsForCertificate%22%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%22Resource%22%3A%20%22%2A%22%0A%20%20%20%20%7D%0A%7D"
          ),
        },
        "arn:aws:iam::aws:policy/test-policy-4" => {
          "v1" => OpenStruct.new(
            # This is arn:aws:iam::aws:policy/PowerUserAccess
            # {
            #   "Version": "2012-10-17",
            #   "Statement": [
            #     {
            #       "Effect": "Allow",
            #       "NotAction": [
            #         "iam:*",
            #         "organizations:*"
            #       ],
            #       "Resource": "*"
            #     },
            #     {
            #       "Effect": "Allow",
            #       "Action": [
            #           "iam:CreateServiceLinkedRole",
            #           "iam:DeleteServiceLinkedRole",
            #           "iam:ListRoles",
            #           "organizations:DescribeOrganization"
            #       ],
            #       "Resource": "*"
            #     }
            #   ]
            # }
            document: "%7B%0A%20%20%22Version%22%3A%20%222012-10-17%22%2C%0A%20%20%22Statement%22%3A%20%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%22NotAction%22%3A%20%5B%22iam%3A%2A%22%2C%20%22organizations%3A%2A%22%5D%2C%0A%20%20%20%20%20%20%22Resource%22%3A%20%22%2A%22%0A%20%20%20%20%7D%2C%7B%0A%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%22Action%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%22iam%3ACreateServiceLinkedRole%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22iam%3ADeleteServiceLinkedRole%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22iam%3AListRoles%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22organizations%3ADescribeOrganization%22%0A%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%22Resource%22%3A%20%22%2A%22%0A%20%20%20%20%7D%0A%20%20%5D%0A%7D"
          ),
        },
      }
      pv = fixtures.dig(query[:policy_arn], query[:version_id])
      return OpenStruct.new(policy_version: pv) if pv
      raise Aws::IAM::Errors::NoSuchEntity.new(nil, nil)
    end
  end
end
