//
// Copyright 2022 The Sigstore Authors.
//
// 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.

package policy

import (
	"context"
	"strings"
	"testing"
)

const (
	customAttestation = `
	{
		"_type": "https://in-toto.io/Statement/v0.1",
		"predicateType": "https://cosign.sigstore.dev/attestation/v1",
		"subject": [
		  {
			"name": "registry.local:5000/policy-controller/demo",
			"digest": {
			  "sha256": "416cc82c76114b1744ea58bcbf2f411a0f2de4b0456703bf1bb83d33656951bc"
			}
		  }
		],
		"predicate": {
		  "Data": "foobar e2e test",
		  "Timestamp": "2022-04-20T18:17:19Z"
		}
	  }`

	vulnAttestation = `
	{
		"_type": "https://in-toto.io/Statement/v0.1",
		"predicateType": "https://cosign.sigstore.dev/attestation/vuln/v1",
		"subject": [
		  {
			"name": "registry.local:5000/policy-controller/demo",
			"digest": {
			  "sha256": "416cc82c76114b1744ea58bcbf2f411a0f2de4b0456703bf1bb83d33656951bc"
			}
		  }
		],
		"predicate": {
		  "invocation": {
			"parameters": null,
			"uri": "invocation.example.com/cosign-testing",
			"event_id": "",
			"builder.id": ""
		  },
		  "scanner": {
			"uri": "fakescanner.example.com/cosign-testing",
			"version": "",
			"db": {
			  "uri": "",
			  "version": ""
			},
			"result": null
		  },
		  "metadata": {
			"scanStartedOn": "2022-04-12T00:00:00Z",
			"scanFinishedOn": "2022-04-12T00:10:00Z"
		  }
		}
	  }`

	cipAttestation = "{\"authorityMatches\":{\"keyatt\":{\"signatures\":null,\"attestations\":{\"vuln-key\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"keysignature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null},\"keylessatt\":{\"signatures\":null,\"attestations\":{\"custom-keyless\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}}}}"
)

func TestEvalPolicy(t *testing.T) {
	// TODO(vaikas): Consider moving the attestations/cue files into testdata
	// directory.
	tests := []struct {
		name        string
		json        string
		policyType  string
		policyFile  string
		wantErr     bool
		wantErrSub  string
		wantWarnSub string
	}{{
		name:       "custom attestation, mismatched predicateType",
		json:       customAttestation,
		policyType: "cue",
		policyFile: `predicateType: "https://cosign.sigstore.dev/attestation/vuln/v1"`,
		wantErr:    true,
		wantErrSub: `conflicting values "https://cosign.sigstore.dev/attestation/v1" and "https://cosign.sigstore.dev/attestation/vuln/v1"`,
	}, {
		name:       "custom attestation, predicateType and data checks out",
		json:       customAttestation,
		policyType: "cue",
		policyFile: `predicateType: "https://cosign.sigstore.dev/attestation/v1"
		predicate: Data: "foobar e2e test"`,
	}, {
		name:       "custom attestation, data mismatch",
		json:       customAttestation,
		policyType: "cue",
		policyFile: `predicateType: "https://cosign.sigstore.dev/attestation/v1"
		predicate: Data: "invalid data here"`,
		wantErr:    true,
		wantErrSub: `predicate.Data: conflicting values "foobar e2e test" and "invalid data here"`,
	}, {
		name:       "vuln attestation, wrong invocation url",
		json:       vulnAttestation,
		policyType: "cue",
		policyFile: `predicateType: "https://cosign.sigstore.dev/attestation/vuln/v1"
		predicate: invocation: uri: "invocation.example.com/wrong-url-here"`,
		wantErr:    true,
		wantErrSub: `conflicting values "invocation.example.com/cosign-testing" and "invocation.example.com/wrong-url-here"`,
	}, {
		name:       "vuln attestation, checks out",
		json:       vulnAttestation,
		policyType: "cue",
		policyFile: `predicateType: "https://cosign.sigstore.dev/attestation/vuln/v1"
		predicate: invocation: uri: "invocation.example.com/cosign-testing"`,
	}, {
		name:       "cluster image policy main policy, checks out",
		json:       cipAttestation,
		policyType: "cue",
		policyFile: `package sigstore
		import "struct"
		import "list"
		authorityMatches: {
		  keyatt: {
			attestations: struct.MaxFields(1) & struct.MinFields(1)
		  },
		  keysignature: {
			signatures: list.MaxItems(1) & list.MinItems(1)
		  },
		  keylessatt: {
			attestations: struct.MaxFields(1) & struct.MinFields(1)
		  },
		  keylesssignature: {
			signatures: list.MaxItems(1) & list.MinItems(1)
		  }
		}`,
	}, {
		name:       "cluster image policy main policy, fails",
		json:       cipAttestation,
		policyType: "cue",
		wantErr:    true,
		wantErrSub: `failed evaluating cue policy for cluster image policy main policy, fails: failed to evaluate the policy with error: authorityMatches.keylessattMinAttestations: conflicting values 2 and "Error" (mismatched types int and string)`,
		policyFile: `package sigstore
		import "struct"
		import "list"
		authorityMatches: {
		  keyatt: {
			attestations: struct.MaxFields(1) & struct.MinFields(1)
		  },
		  keysignature: {
			signatures: list.MaxItems(1) & list.MinItems(1)
		  },
		  if( len(authorityMatches.keylessatt.attestations) < 2) {
			keylessattMinAttestations: 2
			keylessattMinAttestations: "Error"
		  },
		  keylesssignature: {
			signatures: list.MaxItems(1) & list.MinItems(1)
		  }
		}`}, {
		name:       "Rego cluster image policy main policy, checks out",
		json:       cipAttestation,
		policyType: "rego",
		policyFile: `package sigstore
			default isCompliant = false
			isCompliant {
				attestationsKeylessATT := input.authorityMatches.keylessatt.attestations
				count(attestationsKeylessATT) == 1
				attestationsKeyATT := input.authorityMatches.keyatt.attestations
				count(attestationsKeyATT) == 1
				keySignature := input.authorityMatches.keysignature.signatures
				count(keySignature) == 1
			}`,
	},
		{
			name:       "Rego cluster image policy main policy, fails",
			json:       cipAttestation,
			policyType: "rego",
			wantErr:    true,
			wantErrSub: `failed evaluating rego policy for type Rego cluster image policy main policy, fails: policy is not compliant for query 'isCompliant = data.sigstore.isCompliant'`,
			policyFile: `package sigstore
			default isCompliant = false
			isCompliant {
			    attestationsKeylessATT := input.authorityMatches.keylessatt.attestations
				count(attestationsKeylessATT) == 2
				attestationsKeyATT := input.authorityMatches.keyatt.attestations
				count(attestationsKeyATT) == 1
				keySignature := input.authorityMatches.keysignature.signatures
				count(keySignature) == 1
			}`,
		}, {
			name:       "Rego cluster image policy main policy succeed with empty error msg",
			json:       cipAttestation,
			policyType: "rego",
			policyFile: `package sigstore
			isCompliant[response] {
			    attestationsKeylessATT := input.authorityMatches.keylessatt.attestations
				result = (count(attestationsKeylessATT) == 1)
				attestationsKeyATT := input.authorityMatches.keyatt.attestations
				result = (count(attestationsKeyATT) == 1)
				keySignature := input.authorityMatches.keysignature.signatures
				result = (count(keySignature) == 1)

				errorMsg = ""
				warnMsg = ""

				response := {
					"result" : result,
					"error" : errorMsg,
					"warning" : warnMsg
				}
			}`,
		}, {
			name:       "Rego cluster image policy main policy, fails with custom error msg",
			json:       cipAttestation,
			policyType: "rego",
			wantErr:    true,
			wantErrSub: `Not found expected list of attestations`,
			policyFile: `package sigstore
			isCompliant[response] {
			    attestationsKeylessATT := input.authorityMatches.keylessatt.attestations
				result = (count(attestationsKeylessATT) == 1000)

				errorMsg = "Not found expected list of attestations"
				warnMsg = ""

				response := {
					"result" : result,
					"error" : errorMsg,
					"warning" : warnMsg
				}
			}`,
		}, {
			name:        "Rego cluster image policy main policy, returns a custom warning msg",
			json:        cipAttestation,
			policyType:  "rego",
			wantErr:     false,
			wantWarnSub: `Throw warning error even if succeeded`,
			policyFile: `package sigstore
			isCompliant[response] {
				attestationsKeylessATT := input.authorityMatches.keylessatt.attestations
				result = (count(attestationsKeylessATT) == 1)
				attestationsKeyATT := input.authorityMatches.keyatt.attestations
				result = (count(attestationsKeyATT) == 1)
				keySignature := input.authorityMatches.keysignature.signatures
				result = (count(keySignature) == 1)

				errorMsg = ""
				warnMsg = "Throw warning error even if succeeded"

				response := {
					"result" : result,
					"error" : errorMsg,
					"warning" : warnMsg
				}
			}`,
		}}
	for _, tc := range tests {
		ctx := context.Background()
		warn, err := EvaluatePolicyAgainstJSON(ctx, tc.name, tc.policyType, tc.policyFile, []byte(tc.json))
		if tc.wantErr {
			if err == nil {
				t.Errorf("Did not get an error, wanted %s", tc.wantErrSub)
			} else if !strings.Contains(err.Error(), tc.wantErrSub) {
				t.Errorf("Unexpected error, want: %s got: %s", tc.wantErrSub, err.Error())
			}
			if tc.wantWarnSub != "" && !strings.Contains(warn.Error(), tc.wantWarnSub) {
				t.Errorf("Unexpected warning, want: %s got: %s", tc.wantErrSub, err.Error())
			}
		} else {
			if !tc.wantErr && err != nil {
				t.Errorf("Unexpected error, wanted none, got: %s", err.Error())
			}
			if tc.wantWarnSub != "" && !strings.Contains(warn.Error(), tc.wantWarnSub) {
				t.Errorf("Unexpected warning, want: %s got: %s", tc.wantWarnSub, warn.Error())
			}
		}
	}
}
