// Copyright 2024 Google LLC
//
// 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.

package policy

import (
	"fmt"
	"testing"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/ext"
	"github.com/google/go-cmp/cmp"
	"go.yaml.in/yaml/v3"
)

func TestParse(t *testing.T) {
	for _, tst := range policyTests {
		srcFile := readPolicy(t, fmt.Sprintf("testdata/%s/policy.yaml", tst.name))
		parser, err := NewParser(tst.parseOpts...)
		if err != nil {
			t.Fatalf("NewParser() failed: %v", err)
		}
		p, iss := parser.Parse(srcFile)
		if iss.Err() != nil {
			t.Fatalf("parser.Parse() failed: %v", iss.Err())
		}
		if p.Name().Value != tst.name {
			t.Errorf("policy name is %v, wanted %q", p.name, tst.name)
		}
	}
}

func TestParseError(t *testing.T) {
	tests := []struct {
		txt string
		err string
	}{
		{
			txt: `
name:
  illegal: yaml-type`,
			err: `ERROR: <input>:3:3: got yaml node type tag:yaml.org,2002:map, wanted type(s) [tag:yaml.org,2002:str !txt]
 |   illegal: yaml-type
 | ..^`,
		},
		{
			txt: `
rule:
  custom: yaml-type`,
			err: `ERROR: <input>:3:3: unsupported rule tag: custom
 |   custom: yaml-type
 | ..^`,
		},
		{
			txt: `
inputs:
  - name: a
  - name: b`,
			err: `ERROR: <input>:2:1: unsupported policy tag: inputs
 | inputs:
 | ^`,
		},
		{
			txt: `
rule:
  variables:
    - name: "true"
      alt_name: "bool_true"`,
			err: `ERROR: <input>:5:7: unsupported variable tag: alt_name
 |       alt_name: "bool_true"
 | ......^`,
		},
		{
			txt: `
rule:
  match:
    - name: "true"
      alt_name: "bool_true"`,
			err: `ERROR: <input>:4:7: unsupported match tag: name
 |     - name: "true"
 | ......^
ERROR: <input>:4:7: match does not specify a rule or output
 |     - name: "true"
 | ......^
ERROR: <input>:5:7: unsupported match tag: alt_name
 |       alt_name: "bool_true"
 | ......^`,
		},
		{
			txt: `
- rule:
    id: a`,
			err: `ERROR: <input>:2:1: got yaml node type tag:yaml.org,2002:seq, wanted type(s) [tag:yaml.org,2002:map]
 | - rule:
 | ^`,
		},
		{
			txt: `
rule:
  match:
    - condition: "true"
      output: "world"
      rule:
        match:
          - output: "hello"`,
			err: `ERROR: <input>:6:7: only the rule or the output may be set
 |       rule:
 | ......^`,
		},
		{
			txt: `
rule:
  match:
    - condition: "true"
      rule:
        match:
          - output: "hello"
      output: "world"`,
			err: `ERROR: <input>:8:7: only the rule or the output may be set
 |       output: "world"
 | ......^`,
		},
		{
			txt: `
rule:
  match:
    - condition: "true"
      explanation: "hi"
      rule:
        match:
          - output: "hello"`,
			err: `ERROR: <input>:6:7: explanation can only be set on output match cases, not nested rules
 |       rule:
 | ......^`,
		},
		{
			txt: `
rule:
  match:
    - condition: "true"
      rule:
        match:
          - output: "hello"
      explanation: "hi"`,
			err: `ERROR: <input>:8:7: explanation can only be set on output match cases, not nested rules
 |       explanation: "hi"
 | ......^`,
		},
		{
			txt: `
imports:
  - first`,
			err: `ERROR: <input>:3:5: got yaml node type tag:yaml.org,2002:str, wanted type(s) [tag:yaml.org,2002:map]
 |   - first
 | ....^`,
		},
		{
			txt: `
imports:
  first: name`,
			err: `ERROR: <input>:3:3: got yaml node type tag:yaml.org,2002:map, wanted type(s) [tag:yaml.org,2002:seq]
 |   first: name
 | ..^`,
		},
		{
			txt: `
rule:
  - variables: name`,
			err: `ERROR: <input>:3:3: got yaml node type tag:yaml.org,2002:seq, wanted type(s) [tag:yaml.org,2002:map]
 |   - variables: name
 | ..^`,
		},
		{
			txt: `
rule:
  variables: name`,
			err: `ERROR: <input>:3:14: got yaml node type tag:yaml.org,2002:str, wanted type(s) [tag:yaml.org,2002:seq]
 |   variables: name
 | .............^`,
		},
		{
			txt: `
rule:
  variables: 
    - name`,
			err: `ERROR: <input>:4:7: got yaml node type tag:yaml.org,2002:str, wanted type(s) [tag:yaml.org,2002:map]
 |     - name
 | ......^`,
		},
		{
			txt: `
rule:
  match: 
    name: value`,
			err: `ERROR: <input>:4:5: got yaml node type tag:yaml.org,2002:map, wanted type(s) [tag:yaml.org,2002:seq]
 |     name: value
 | ....^`,
		},
		{
			txt: `
rule:
  match: 
    - name`,
			err: `ERROR: <input>:4:7: got yaml node type tag:yaml.org,2002:str, wanted type(s) [tag:yaml.org,2002:map]
 |     - name
 | ......^`,
		},
	}

	for _, tst := range tests {
		parser, err := NewParser()
		if err != nil {
			t.Fatalf("NewParser() failed: %v", err)
		}
		_, iss := parser.Parse(StringSource(tst.txt, "<input>"))
		if iss.Err() == nil {
			t.Fatalf("parser.Parse(%q) did not error, wanted %s", tst.txt, tst.err)
		}

		if iss.Err().Error() != tst.err {
			t.Errorf("parser.Parse(%q) got error %v, wanted error %s", tst.txt, iss.Err(), tst.err)
		}
	}
}

func TestGetExplanationOutputPolicy(t *testing.T) {
	tst := `
rule:
  match:
    - condition: "false"
      rule:
        match:
          - condition: "1 > 2"
            output: "false"
            explanation: "'bad_inner'"
          - output: "true"
            explanation: "'good_inner'"
    - output: "true"
      explanation: "'good_outer'"
    `

	parser, err := NewParser()
	if err != nil {
		t.Fatalf("NewParser() failed: %v", err)
	}
	policy, iss := parser.Parse(StringSource(tst, "<input>"))
	if iss != nil {
		t.Fatalf("Parse() failed: %v", err)
	}

	explanationPolicy := policy.GetExplanationOutputPolicy()

	want := "'bad_inner'"
	got := explanationPolicy.Rule().Matches()[0].rule.Matches()[0].output.Value
	if got != want {
		t.Errorf("First inner output = %v, wanted %v", got, want)
	}

	want = "1 > 2"
	got = explanationPolicy.Rule().Matches()[0].rule.Matches()[0].condition.Value
	if got != want {
		t.Errorf("First inner condition = %v, wanted %v", got, want)
	}

	want = "'good_inner'"
	got = explanationPolicy.Rule().Matches()[0].rule.Matches()[1].output.Value
	if got != want {
		t.Errorf("Second inner output = %v, wanted %v", got, want)
	}

	want = "'good_outer'"
	got = explanationPolicy.Rule().Matches()[1].output.Value
	if got != want {
		t.Errorf("Second outer output = %v, wanted %v", got, want)
	}
}

func TestCustomTagVisitor(t *testing.T) {
	tst := `name: "test"
description: |-2
   A test description.
version: 1
version: 2
last-modified: 2026-02-09
rule:
  match:
    - condition: "true"
      output: "true"
`

	handler := &testTagHandler{}
	parser, err := NewParser(CustomTagVisitor(handler))
	if err != nil {
		t.Fatalf("NewParser() failed: %v", err)
	}
	policy, iss := parser.Parse(StringSource(tst, "<input>"))
	if iss != nil {
		t.Fatalf("Parse() failed: %v", iss.Err())
	}

	if dx := cmp.Diff(" A test description.", handler.description); dx != "" {
		t.Errorf("handler.description (+got, -want): %s", dx)
	}
	if dx := cmp.Diff(" A test description.", policy.Description().Value); dx != "" {
		t.Errorf("policy.Description() (+got, -want): %s", dx)
	}

	if len(policy.MetadataKeys()) != 3 {
		t.Errorf("policy.MetadataKeys() got %v, wanted 2 keys", policy.MetadataKeys())
	}
	prevVer, found := policy.Metadata("prev.version")
	if !found {
		t.Fatalf("prev.version not found in policy.Metadata")
	}
	if prevVer != "1" {
		t.Errorf("prev.version got %s, wanted 1", prevVer)
	}
}

func TestSimpleVariables(t *testing.T) {
	tst := `name: "test"
rule:
  variables:
    - first: "1.5"
    - second: "2.5"
  match:
    - output: >
        variables.first + variables.second

`

	parser, err := NewParser(SimpleVariables())
	if err != nil {
		t.Fatalf("NewParser() failed: %v", err)
	}
	policy, iss := parser.Parse(StringSource(tst, "<input>"))
	if iss != nil {
		t.Fatalf("Parse() failed: %v", iss.Err())
	}
	env, err := cel.NewEnv(ext.Bindings())
	if err != nil {
		t.Fatalf("NewEnv() failed: %v", err)
	}
	ast, iss := Compile(env, policy)
	if iss != nil {
		t.Fatalf("Compile() failed: %v", iss.Err())
	}
	prg, err := env.Program(ast)
	if err != nil {
		t.Fatalf("Program() failed: %v", err)
	}
	out, _, err := prg.Eval(cel.NoVars())
	if err != nil {
		t.Fatalf("prg.Eval() failed: %v", err)
	}
	if out != types.Double(4.0) {
		t.Errorf("got %v, wanted 4.0", out)
	}
}

type testTagHandler struct {
	defaultTagVisitor

	description string
}

func (t *testTagHandler) PolicyTag(ctx ParserContext, id int64, tagName string, node *yaml.Node, p *Policy) {
	if tagName == "description" {
		t.description = node.Value
		return
	}
	// Store the last value for a repeated tag
	if meta, found := p.Metadata(tagName); found {
		p.SetMetadata("prev."+tagName, meta)
		p.SetMetadata(tagName, node.Value)
	} else {
		p.SetMetadata(tagName, node.Value)
	}
}
