package flag_test

import (
	"testing"

	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
	"github.com/aquasecurity/trivy/pkg/compliance/spec"
	"github.com/aquasecurity/trivy/pkg/flag"
	iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
	"github.com/aquasecurity/trivy/pkg/log"
	"github.com/aquasecurity/trivy/pkg/types"
	xstrings "github.com/aquasecurity/trivy/pkg/x/strings"
)

func TestReportFlagGroup_ToOptions(t *testing.T) {
	type fields struct {
		format           types.Format
		template         string
		dependencyTree   bool
		listAllPkgs      bool
		ignoreUnfixed    bool
		ignoreFile       string
		exitCode         int
		exitOnEOSL       bool
		ignorePolicy     string
		output           string
		outputPluginArgs string
		severities       string
		compliance       string
		debug            bool
		pkgTypes         string
		tableModes       []string
	}
	tests := []struct {
		name     string
		fields   fields
		want     flag.ReportOptions
		wantErr  string
		wantLogs []string
	}{
		{
			name:   "happy default (without flags)",
			fields: fields{},
			want:   flag.ReportOptions{},
		},
		{
			name: "happy path with an cyclonedx",
			fields: fields{
				severities: "CRITICAL",
				format:     "cyclonedx",
			},
			want: flag.ReportOptions{
				Severities: []dbTypes.Severity{dbTypes.SeverityCritical},
				Format:     types.FormatCycloneDX,
			},
		},
		{
			name: "invalid option combination: --template enabled without --format",
			fields: fields{
				template:   "@contrib/gitlab.tpl",
				severities: "LOW",
			},
			wantLogs: []string{
				"'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.",
			},
			want: flag.ReportOptions{
				Severities: []dbTypes.Severity{dbTypes.SeverityLow},
				Template:   "@contrib/gitlab.tpl",
			},
		},
		{
			name: "invalid option combination: --template and --format json",
			fields: fields{
				format:     "json",
				template:   "@contrib/gitlab.tpl",
				severities: "LOW",
			},
			wantLogs: []string{
				"'--template' is ignored because '--format json' is specified. Use '--template' option with '--format template' option.",
			},
			want: flag.ReportOptions{
				Format:     "json",
				Severities: []dbTypes.Severity{dbTypes.SeverityLow},
				Template:   "@contrib/gitlab.tpl",
			},
		},
		{
			name: "invalid option combination: --format template without --template",
			fields: fields{
				format:     "template",
				severities: "LOW",
			},
			wantLogs: []string{
				"'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.",
			},
			want: flag.ReportOptions{
				Format:     "template",
				Severities: []dbTypes.Severity{dbTypes.SeverityLow},
			},
		},
		{
			name: "invalid option combination: --list-all-pkgs with --format table",
			fields: fields{
				format:      "table",
				severities:  "LOW",
				listAllPkgs: true,
			},
			wantLogs: []string{
				`"--list-all-pkgs" is only valid for the JSON format, for other formats a list of packages is automatically included.`,
			},
			want: flag.ReportOptions{
				Format:      "table",
				Severities:  []dbTypes.Severity{dbTypes.SeverityLow},
				ListAllPkgs: true,
			},
		},
		{
			name: "happy path with output plugin args",
			fields: fields{
				output:           "plugin=count",
				outputPluginArgs: "--publish-after 2023-10-01 --publish-before 2023-10-02",
			},
			want: flag.ReportOptions{
				Output: "plugin=count",
				OutputPluginArgs: []string{
					"--publish-after",
					"2023-10-01",
					"--publish-before",
					"2023-10-02",
				},
			},
		},
		{
			name: "happy path with compliance",
			fields: fields{
				compliance: "@testdata/example-spec.yaml",
				severities: dbTypes.SeverityLow.String(),
			},
			want: flag.ReportOptions{
				Compliance: spec.ComplianceSpec{
					Spec: iacTypes.Spec{
						ID:          "0001",
						Title:       "my-custom-spec",
						Description: "My fancy spec",
						Version:     "1.2",
						Controls: []iacTypes.Control{
							{
								ID:          "1.1",
								Name:        "Unencrypted S3 bucket",
								Description: "S3 Buckets should be encrypted to protect the data that is stored within them if access is compromised.",
								Checks: []iacTypes.SpecCheck{
									{ID: "AVD-AWS-0088"},
								},
								Severity: "HIGH",
							},
						},
					},
				},
				Severities: []dbTypes.Severity{dbTypes.SeverityLow},
			},
		},
		{
			name: "invalid option combination: --table-modes with --format json",
			fields: fields{
				format:     "json",
				tableModes: xstrings.ToStringSlice(types.SupportedTableModes),
			},
			wantErr: `"--table-mode" can be used only with "--format table".`,
		},
		{
			name: "happy path with template file (.tpl extension)",
			fields: fields{
				format:     "template",
				template:   "@contrib/gitlab.tpl",
				severities: "HIGH",
			},
			want: flag.ReportOptions{
				Format:     "template",
				Template:   "@contrib/gitlab.tpl",
				Severities: []dbTypes.Severity{dbTypes.SeverityHigh},
			},
		},
		{
			name: "error: template file without .tpl extension",
			fields: fields{
				format:   "template",
				template: "@/etc/passwd",
			},
			wantErr: "template file must have .tpl extension: /etc/passwd",
		},
		{
			name: "error: template file with wrong extension",
			fields: fields{
				format:   "template",
				template: "@report.txt",
			},
			wantErr: "template file must have .tpl extension: report.txt",
		},
		{
			name: "inline template (no @ prefix) is allowed",
			fields: fields{
				format:     "template",
				template:   "{{ .Results }}",
				severities: "MEDIUM",
			},
			want: flag.ReportOptions{
				Format:     "template",
				Template:   "{{ .Results }}",
				Severities: []dbTypes.Severity{dbTypes.SeverityMedium},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Cleanup(viper.Reset)

			level := log.LevelWarn
			if tt.fields.debug {
				level = log.LevelDebug
			}
			out := newLogger(level)

			setValue(flag.FormatFlag.ConfigName, string(tt.fields.format))
			setValue(flag.TemplateFlag.ConfigName, tt.fields.template)
			setValue(flag.DependencyTreeFlag.ConfigName, tt.fields.dependencyTree)
			setValue(flag.ListAllPkgsFlag.ConfigName, tt.fields.listAllPkgs)
			setValue(flag.IgnoreFileFlag.ConfigName, tt.fields.ignoreFile)
			setValue(flag.IgnoreUnfixedFlag.ConfigName, tt.fields.ignoreUnfixed)
			setValue(flag.IgnorePolicyFlag.ConfigName, tt.fields.ignorePolicy)
			setValue(flag.ExitCodeFlag.ConfigName, tt.fields.exitCode)
			setValue(flag.ExitOnEOLFlag.ConfigName, tt.fields.exitOnEOSL)
			setValue(flag.OutputFlag.ConfigName, tt.fields.output)
			setValue(flag.OutputPluginArgFlag.ConfigName, tt.fields.outputPluginArgs)
			setValue(flag.SeverityFlag.ConfigName, tt.fields.severities)
			setValue(flag.ComplianceFlag.ConfigName, tt.fields.compliance)
			setSliceValue(flag.TableModeFlag.ConfigName, tt.fields.tableModes)

			// Assert options
			f := &flag.ReportFlagGroup{
				Format:          flag.FormatFlag.Clone(),
				Template:        flag.TemplateFlag.Clone(),
				DependencyTree:  flag.DependencyTreeFlag.Clone(),
				ListAllPkgs:     flag.ListAllPkgsFlag.Clone(),
				IgnoreFile:      flag.IgnoreFileFlag.Clone(),
				IgnorePolicy:    flag.IgnorePolicyFlag.Clone(),
				ExitCode:        flag.ExitCodeFlag.Clone(),
				ExitOnEOL:       flag.ExitOnEOLFlag.Clone(),
				Output:          flag.OutputFlag.Clone(),
				OutputPluginArg: flag.OutputPluginArgFlag.Clone(),
				Severity:        flag.SeverityFlag.Clone(),
				Compliance:      flag.ComplianceFlag.Clone(),
				TableMode:       flag.TableModeFlag.Clone(),
			}

			flags := flag.Flags{f}
			got, err := flags.ToOptions(nil)
			if tt.wantErr != "" {
				require.ErrorContains(t, err, tt.wantErr)
				return
			}

			assert.EqualExportedValues(t, tt.want, got.ReportOptions)

			// Assert log messages
			assert.Equal(t, tt.wantLogs, out.Messages(), tt.name)
		})
	}

	t.Run("Error on non existing ignore file", func(t *testing.T) {
		t.Cleanup(viper.Reset)

		setValue(flag.IgnoreFileFlag.ConfigName, "doesntexist")
		f := &flag.ReportFlagGroup{
			IgnoreFile: flag.IgnoreFileFlag.Clone(),
		}

		flags := flag.Flags{f}
		_, err := flags.ToOptions(nil)
		assert.ErrorContains(t, err, "ignore file not found: doesntexist")
	})
}
