package policy

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"testing"
)

// ensure we don't loose information after parsing and remarshaling
// we marshal only for testing, the output will not necessarily be a valid IAM policy
func TestParsePolicy(t *testing.T) {
	tests := []struct {
		filename string
		want     string
	}{
		{
			filename: "amazon-ec2-full-access.json",
			want:     `{"Version":"2012-10-17","Id":"","Statement":[{"Effect":"Allow","Principal":{},"Action":["ec2:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["elasticloadbalancing:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["cloudwatch:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["autoscaling:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["iam:CreateServiceLinkedRole"],"Resource":["*"],"Condition":{"StringEquals":{"iam:AWSServiceName":["autoscaling.amazonaws.com","ec2scheduled.amazonaws.com","elasticloadbalancing.amazonaws.com","spot.amazonaws.com","spotfleet.amazonaws.com","transitgateway.amazonaws.com"]}}}]}`,
		},
		{
			filename: "sns-conditionally-public.json",
			want:     `{"Version":"","Id":"","Statement":[{"Effect":"Allow","Principal":"*","Action":["SNS:Publish"],"Resource":["arn:aws:sns:us-east-1:123456789012:MyFirstTopic"],"Condition":{"StringEquals":{"aws:sourceVpce":["vpce-1a2b3c4d"]}}}]}`,
		},
		{
			filename: "sns-shared-via-condition.json",
			want:     `{"Version":"","Id":"","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["SNS:Publish"],"Resource":["arn:aws:sns:us-east-2:444455556666:MyTopic"],"Condition":{"ArnLike":{"aws:SourceArn":["arn:aws:cloudwatch:us-east-2:111122223333:alarm:*"]}}}]}`,
		},
		{
			filename: "sns-shared-with-org.json",
			want:     `{"Version":"","Id":"","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["SNS:Publish"],"Resource":["arn:aws:sns:us-east-2:444455556666:MyTopic"],"Condition":{"StringEquals":{"aws:PrincipalOrgID":["o-yj4fwt1bwm"]}}}]}`,
		},
		{
			filename: "sqs-conditionally-public.json",
			want:     `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"conditionally_public","Effect":"Allow","Principal":"*","Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"],"Condition":{"IpAddress":{"aws:SourceIp":["192.0.2.0/24"]}}}]}`,
		},
		{
			filename: "sqs-public.json",
			want:     `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"unconditionally_public","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"]}]}`,
		},
		{
			filename: "sqs-shared-with-condition.json",
			want:     `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"conditionally_shared","Effect":"Allow","Principal":{"AWS":["123456789012"]},"Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"],"Condition":{"IpAddress":{"aws:SourceIp":["192.0.2.0/24"]}}}]}`,
		},
		{
			filename: "sqs-shared.json",
			want:     `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"unconditionally_shared","Effect":"Allow","Principal":{"AWS":["111122223333","arn:aws:iam::123456789012:root"]},"Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"]}]}`,
		},
	}

	for _, tt := range tests {
		policy, err := getTestFixure(tt.filename)
		if err != nil {
			t.Errorf("Error getting test fixture %s: %s", tt.filename, err)
			continue
		}

		actual, err := json.Marshal(policy)
		if err != nil {
			t.Errorf("Error parsing test fixture %s: %s", tt.filename, err)
			continue
		}

		if tt.want != string(actual) {
			t.Errorf("Policy %s parsed in unexpected way:\n%s\n---\n%s\n---", tt.filename, actual, tt.want)
			continue
		}
	}
}

func TestIsConditionallyPublic(t *testing.T) {
	tests := []struct {
		filename string
		want     bool
	}{
		{filename: "amazon-ec2-full-access.json", want: false},
		{filename: "sns-conditionally-public.json", want: true},
		{filename: "sns-shared-via-condition.json", want: false},
		{filename: "sns-shared-with-org.json", want: false},
		{filename: "sqs-conditionally-public.json", want: true},
		{filename: "sqs-public.json", want: false},
		{filename: "sqs-shared-with-condition.json", want: false},
		{filename: "sqs-shared.json", want: false},
	}

	for _, tt := range tests {
		policy, err := getTestFixure(tt.filename)
		if err != nil {
			t.Errorf("Error getting test fixture %s: %s", tt.filename, err)
			continue
		}

		actual := policy.IsConditionallyPublic()

		if tt.want != actual {
			t.Errorf("IsConditionallyPublic(%s) is %v but should be %v", tt.filename, actual, tt.want)
			continue
		}
	}
}

func TestIsPublic(t *testing.T) {
	tests := []struct {
		filename string
		want     bool
	}{
		{filename: "amazon-ec2-full-access.json", want: false},
		{filename: "sns-conditionally-public.json", want: false},
		{filename: "sns-shared-via-condition.json", want: false},
		{filename: "sns-shared-with-org.json", want: false},
		{filename: "sqs-conditionally-public.json", want: false},
		{filename: "sqs-public.json", want: true},
		{filename: "sqs-shared-with-condition.json", want: false},
		{filename: "sqs-shared.json", want: false},
	}

	for _, tt := range tests {
		policy, err := getTestFixure(tt.filename)
		if err != nil {
			t.Errorf("Error getting test fixture %s: %s", tt.filename, err)
			continue
		}

		actual := policy.IsPublic()

		if tt.want != actual {
			t.Errorf("IsPublic(%s) is %v but should be %v", tt.filename, actual, tt.want)
			continue
		}
	}
}

func getTestFixure(filename string) (*Policy, error) {
	f, err := os.Open("testdata/" + filename)
	if err != nil {
		return nil, fmt.Errorf("opening file: %s", err)
	}
	defer f.Close()

	data, err := ioutil.ReadAll(f)
	if err != nil {
		return nil, fmt.Errorf("reading file: %s", err)
	}

	policy, err := ParseJSONPolicy(data)
	if err != nil {
		return nil, fmt.Errorf("parsing JSON: %s", err)
	}

	return &policy, nil
}
func TestDoesPolicyHaveMatchingStatement(t *testing.T) {
	p := &Policy{
		Statement: []PolicyStatement{
			{
				Effect:   "Allow",
				Action:   []string{"ec2:*"},
				Resource: []string{"*"},
			},
			{
				Effect:   "Allow",
				Action:   []string{"s3:GetObject"},
				Resource: []string{"arn:aws:s3:::bucket/*"},
			},
			{
				Effect:   "Deny",
				Action:   []string{"s3:*"},
				Resource: []string{"arn:aws:s3:::bucket2/*"},
			},
		},
	}

	tests := []struct {
		effect          string
		actionToCheck   string
		resourceToCheck string
		want            bool
	}{
		{
			effect:          "Allow",
			actionToCheck:   "ec2:DescribeInstances",
			resourceToCheck: "arn:aws:ec2:us-west-2:123456789012:instance/*",
			want:            true,
		},
		{
			effect:          "Allow",
			actionToCheck:   "s3:GetObject",
			resourceToCheck: "arn:aws:s3:::bucket/file.txt",
			want:            true,
		},
		{
			effect:          "Allow",
			actionToCheck:   "s3:PutObject",
			resourceToCheck: "arn:aws:s3:::bucket/file.txt",
			want:            false,
		},
		{
			effect:          "Deny",
			actionToCheck:   "s3:GetObject",
			resourceToCheck: "arn:aws:s3:::bucket2/file.txt",
			want:            true,
		},
		{
			effect:          "Deny",
			actionToCheck:   "s3:PutObject",
			resourceToCheck: "arn:aws:s3:::bucket2/file.txt",
			want:            true,
		},
		{
			effect:          "Deny",
			actionToCheck:   "s3:GetObject",
			resourceToCheck: "arn:aws:s3:::bucket/file.txt",
			want:            false,
		},
	}

	for _, tt := range tests {
		actual := p.DoesPolicyHaveMatchingStatement(tt.effect, tt.actionToCheck, tt.resourceToCheck)
		if tt.want != actual {
			t.Errorf("DoesPolicyHaveMatchingStatement(%s, %s, %s) is %v but should be %v", tt.effect, tt.actionToCheck, tt.resourceToCheck, actual, tt.want)
		}
	}
}

func TestDoesPermissionExpansionMatch(t *testing.T) {

	tests := []struct {
		actionToExpand       string
		actionToCheckAgainst string
		want                 bool
	}{
		{
			actionToExpand:       "sts:AssumeRole",
			actionToCheckAgainst: "sts:AssumeRole",
			want:                 true,
		},
		{
			actionToExpand:       "*",
			actionToCheckAgainst: "sts:AssumeRole",
			want:                 true,
		},
		{
			actionToExpand:       "sts:Assume*",
			actionToCheckAgainst: "sts:AssumeRole",
			want:                 true,
		},
		{
			actionToExpand:       "sts:*",
			actionToCheckAgainst: "sts:AssumeRole",
			want:                 true,
		},
		{
			actionToExpand:       "s3:GetObject",
			actionToCheckAgainst: "sts:AssumeRole",
			want:                 false,
		},
		{
			actionToExpand:       "s3:*",
			actionToCheckAgainst: "sts:AssumeRole",
			want:                 false,
		},
	}

	for _, tt := range tests {
		actual := MatchesAfterExpansion(tt.actionToCheckAgainst, tt.actionToExpand)
		if tt.want != actual {
			//fmt.Printf("DoesPermissionHaveMatchingStatement(%s, %s) is %v but should be %v", tt.actionToExpand, tt.actionToCheckAgainst, actual, tt.want)
			t.Errorf("DoesPermissionHaveMatchingStatement(%s, %s) is %v but should be %v", tt.actionToExpand, tt.actionToCheckAgainst, actual, tt.want)
		}
	}
}
