// Copyright 2025 Google LLC
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//  1. Redistributions of source code must retain the above copyright notice, this
//     list of conditions and the following disclaimer.
//
//  2. Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//  3. Neither the name of the copyright holder nor the names of its
//     contributors may be used to endorse or promote products derived from
//     this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package main

import (
	"testing"
	"time"

	"dawn.googlesource.com/dawn/tools/src/container"
	"dawn.googlesource.com/dawn/tools/src/oswrapper"
	"github.com/stretchr/testify/require"
)

// invoke() and things that call it cannot be unittested due to the call to
// exec.CommandContext().

func TestExtractValidationHashes(t *testing.T) {
	tests := []struct {
		name       string
		input      string
		wantString string
		wantHashes []string
	}{
		{
			name:       "No matches",
			input:      "ASDF",
			wantString: "ASDF",
			wantHashes: nil,
		},
		{
			name:       "Single match",
			input:      "Line 1\n<<HASH: ASDF>>\nLine 3",
			wantString: "Line 1\nLine 3",
			wantHashes: []string{"ASDF"},
		},
		{
			name:       "Multiple matches",
			input:      "Line 1\n<<HASH: ASDF>>\nLine 3\n<<HASH: QWER>>\nLine 5",
			wantString: "Line 1\nLine 3\nLine 5",
			wantHashes: []string{"ASDF", "QWER"},
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			out, hashes := extractValidationHashes(testCase.input)
			require.Equal(t, testCase.wantString, out)
			require.Equal(t, testCase.wantHashes, hashes)
		})
	}
}

func TestIndent(t *testing.T) {
	tests := []struct {
		name         string
		input        string
		indentAmount int
		want         string
	}{
		{
			name:         "No indent single line",
			input:        "Foo",
			indentAmount: 0,
			want:         "Foo",
		},
		{
			name:         "Indent single line",
			input:        "Foo",
			indentAmount: 2,
			want:         "  Foo",
		},
		{
			name:         "No indent multi line",
			input:        "Foo\nBar",
			indentAmount: 0,
			want:         "Foo\nBar",
		},
		{
			name:         "Indent multi line",
			input:        "Foo\nBar",
			indentAmount: 2,
			want:         "  Foo\n  Bar",
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			require.Equal(t, testCase.want, indent(testCase.input, testCase.indentAmount))
		})
	}
}

func TestAlignLeft(t *testing.T) {
	tests := []struct {
		name  string
		input string
		width int
		want  string
	}{
		{
			name:  "Width smaller than string length",
			input: "Foo",
			width: 2,
			want:  "Foo",
		},
		{
			name:  "Width equal to string length",
			input: "Foo",
			width: 3,
			want:  "Foo",
		},
		{
			name:  "Width greater than string length",
			input: "Foo",
			width: 5,
			want:  "Foo  ",
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			require.Equal(t, testCase.want, alignLeft(testCase.input, testCase.width))
		})
	}
}

func TestAlignCenter(t *testing.T) {
	tests := []struct {
		name  string
		input string
		width int
		want  string
	}{
		{
			name:  "Width smaller than string length",
			input: "Foo",
			width: 2,
			want:  "Foo",
		},
		{
			name:  "Width equal to string length",
			input: "Foo",
			width: 3,
			want:  "Foo",
		},
		{
			name:  "Width greater than string length",
			input: "Foo",
			width: 5,
			want:  " Foo ",
		},
		{
			name:  "Width greater than string length non-equal padding",
			input: "Foo",
			width: 6,
			want:  " Foo  ",
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			require.Equal(t, testCase.want, alignCenter(testCase.input, testCase.width))
		})
	}
}

func TestAlignRight(t *testing.T) {
	tests := []struct {
		name  string
		input string
		width int
		want  string
	}{
		{
			name:  "Width smaller than string length",
			input: "Foo",
			width: 2,
			want:  "Foo",
		},
		{
			name:  "Width equal to string length",
			input: "Foo",
			width: 3,
			want:  "Foo",
		},
		{
			name:  "Width greater than string length",
			input: "Foo",
			width: 5,
			want:  "  Foo",
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			require.Equal(t, testCase.want, alignRight(testCase.input, testCase.width))
		})
	}
}

func TestMaxStringLen(t *testing.T) {
	tests := []struct {
		name  string
		input []string
		want  int
	}{
		{
			name:  "Nil",
			input: nil,
			want:  0,
		},
		{
			name:  "Empty",
			input: []string{},
			want:  0,
		},
		{
			name: "Multiple lines",
			input: []string{
				"Line 1",
				"AAAAAAAAAAAA",
				"Line 3",
			},
			want: 12,
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			require.Equal(t, testCase.want, maxStringLen(testCase.input))
		})
	}
}

func TestFormatWidth(t *testing.T) {
	tests := []struct {
		name  string
		input outputFormat
		want  int
	}{
		{
			name:  "Less than minimum",
			input: "",
			want:  6,
		},
		{
			name:  "Equal to minimum",
			input: "123456",
			want:  6,
		},
		{
			name:  "Greater than minimum",
			input: "1234567890",
			want:  10,
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			require.Equal(t, testCase.want, formatWidth(testCase.input))
		})
	}
}

func TestPercentage(t *testing.T) {
	tests := []struct {
		name       string
		inputN     int
		inputTotal int
		want       string
	}{
		{
			name:       "Zero total",
			inputN:     10,
			inputTotal: 0,
			want:       "-",
		},
		{
			name:       "Zero N",
			inputN:     0,
			inputTotal: 10,
			want:       "0.0%",
		},
		{
			name:       "Both positive",
			inputN:     1,
			inputTotal: 10,
			want:       "10.0%",
		},
		{
			name:       "Negative N",
			inputN:     -1,
			inputTotal: 10,
			want:       "-10.0%",
		},
		{
			name:       "Negative total",
			inputN:     1,
			inputTotal: -10,
			want:       "-10.0%",
		},
		{
			name:       "Both negative",
			inputN:     -1,
			inputTotal: -10,
			want:       "10.0%",
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			require.Equal(t, testCase.want, percentage(testCase.inputN, testCase.inputTotal))
		})
	}
}

func TestParseFlags(t *testing.T) {
	tests := []struct {
		name          string
		path          string
		contents      string
		skipFileWrite bool
		want          []cmdLineFlags
		wantErr       bool
		wantErrMsg    string
	}{
		{
			name:          "Non-existent file",
			path:          "/foo.txt",
			skipFileWrite: true,
			want:          nil,
			wantErr:       true,
			wantErrMsg:    "open /foo.txt: file does not exist",
		},
		{
			name: "Single entry single flag no format",
			path: "/foo.txt",
			contents: `// Comment
// flags: --hlsl-shader-model 60
// Another comment
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(allOutputFormats...),
					flags:   []string{"", "--hlsl-shader-model", "60"},
				},
			},
		},
		{
			name: "Single entry multiple flags no format",
			path: "/foo.txt",
			contents: `// Comment
// flags: --hlsl-shader-model 60 --overrides wgsize=10
// Another comment
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(allOutputFormats...),
					flags:   []string{"", "--hlsl-shader-model", "60", "--overrides", "wgsize=10"},
				},
			},
		},
		{
			name: "Single entry single flag with format",
			path: "/foo.txt",
			contents: `// Comment
// [wgsl] flags: --hlsl-shader-model 60
// Another comment
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(wgsl),
					flags:   []string{"", "--hlsl-shader-model", "60"},
				},
			},
		},
		{
			name: "Single entry multiple flags with format",
			path: "/foo.txt",
			contents: `// Comment
// [wgsl] flags: --hlsl-shader-model 60 --overrides wgsize=10
// Another comment
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(wgsl),
					flags:   []string{"", "--hlsl-shader-model", "60", "--overrides", "wgsize=10"},
				},
			},
		},
		{
			name: "Multiple entries single flag no format",
			path: "/foo.txt",
			contents: `// Comment
// flags: --hlsl-shader-model 60
// Another comment
// flags: --overrides wgsize=10
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(allOutputFormats...),
					flags:   []string{"", "--hlsl-shader-model", "60"},
				},
				{
					formats: container.NewSet(allOutputFormats...),
					flags:   []string{"", "--overrides", "wgsize=10"},
				},
			},
		},
		{
			name: "Multiple entries multiple flags no format",
			path: "/foo.txt",
			contents: `// Comment
// flags: --hlsl-shader-model 60 --foo
// Another comment
// flags: --overrides wgsize=10 --bar
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(allOutputFormats...),
					flags:   []string{"", "--hlsl-shader-model", "60", "--foo"},
				},
				{
					formats: container.NewSet(allOutputFormats...),
					flags:   []string{"", "--overrides", "wgsize=10", "--bar"},
				},
			},
		},
		{
			name: "Multiple entries single flag with format",
			path: "/foo.txt",
			contents: `// Comment
// [wgsl] flags: --hlsl-shader-model 60
// Another comment
// [glsl] flags: --overrides wgsize=10
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(wgsl),
					flags:   []string{"", "--hlsl-shader-model", "60"},
				},
				{
					formats: container.NewSet(glsl),
					flags:   []string{"", "--overrides", "wgsize=10"},
				},
			},
		},
		{
			name: "Multiple entries multiple flags with format",
			path: "/foo.txt",
			contents: `// Comment
// [wgsl] flags: --hlsl-shader-model 60 --foo
// Another comment
// [glsl] flags: --overrides wgsize=10 --bar
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(wgsl),
					flags:   []string{"", "--hlsl-shader-model", "60", "--foo"},
				},
				{
					formats: container.NewSet(glsl),
					flags:   []string{"", "--overrides", "wgsize=10", "--bar"},
				},
			},
		},
		{
			name: "Multiple entries multiple flags mixed formats",
			path: "/foo.txt",
			contents: `// Comment
// flags: --hlsl-shader-model 60 --foo
// Another comment
// [glsl] flags: --overrides wgsize=10 --bar
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(allOutputFormats...),
					flags:   []string{"", "--hlsl-shader-model", "60", "--foo"},
				},
				{
					formats: container.NewSet(glsl),
					flags:   []string{"", "--overrides", "wgsize=10", "--bar"},
				},
			},
		},
		{
			name: "Invalid format",
			path: "/foo.txt",
			contents: `// Comment
// [invalid] flags: --hlsl-shader-model 60
// Another comment
`,
			want:       nil,
			wantErr:    true,
			wantErrMsg: "unknown format 'invalid'",
		},
		{
			name: "-ir formats automatically included",
			path: "/foo.txt",
			contents: `// Comment
// [hlsl-dxc] flags: --hlsl-shader-model 60
// Another comment
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(hlslDXC),
					flags:   []string{"", "--hlsl-shader-model", "60"},
				},
			},
		},
		{
			name: "Ignored after first comment block",
			path: "/foo.txt",
			contents: `// Comment
// [wgsl] flags: --hlsl-shader-model 60
// Another comment
foo
// [glsl] flags: --hlsl-shader-model 60
`,
			want: []cmdLineFlags{
				{
					formats: container.NewSet(wgsl),
					flags:   []string{"", "--hlsl-shader-model", "60"},
				},
			},
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			wrapper := oswrapper.CreateFSTestOSWrapper()
			if !testCase.skipFileWrite {
				wrapper.WriteFile(testCase.path, []byte(testCase.contents), 0o700)
			}

			flags, err := parseFlags(testCase.path, wrapper)
			if testCase.wantErr {
				require.ErrorContains(t, err, testCase.wantErrMsg)
			} else {
				require.NoErrorf(t, err, "Got error parsing flags: %v", err)
			}
			require.Equal(t, testCase.want, flags)
		})
	}
}

func TestPrintDuration(t *testing.T) {
	tests := []struct {
		name         string
		inputToParse string
		want         string
	}{
		{
			name:         "Seconds",
			inputToParse: "5s",
			want:         "5s",
		},
		{
			name:         "Minutes",
			inputToParse: "5m",
			want:         "5m",
		},
		{
			name:         "Minutes and Seconds",
			inputToParse: "5m5s",
			want:         "5m5s",
		},
		{
			name:         "Hours",
			inputToParse: "5h",
			want:         "5h",
		},
		{
			name:         "Hours and Minutes",
			inputToParse: "5h5m",
			want:         "5h5m",
		},
		{
			name:         "Hours and Seconds",
			inputToParse: "5h5s",
			want:         "5h5s",
		},
		{
			name:         "Hours, Minutes, and Seconds",
			inputToParse: "5h5m5s",
			want:         "5h5m5s",
		},
	}

	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			input, err := time.ParseDuration(testCase.inputToParse)
			require.NoErrorf(t, err, "Failed to parse duration: %v", err)
			require.Equal(t, testCase.want, printDuration(input))
		})
	}
}
