// Copyright 2023 Google LLC
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package common

import (
	"context"
	"os"
	"path/filepath"
	"testing"

	exec_testutils "go.skia.org/infra/go/exec/testutils"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.skia.org/infra/go/exec"
	"go.skia.org/infra/task_driver/go/lib/os_steps"
	"go.skia.org/infra/task_driver/go/td"
	"go.skia.org/skia/infra/bots/task_drivers/testutils"
)

func TestUploadToGold_NoOutputsZIPOrDir_NoGoldctlInvocations(t *testing.T) {
	test := func(name string, utgArgs UploadToGoldArgs) {
		t.Run(name, func(t *testing.T) {
			commandCollector := exec.CommandCollector{}
			res := td.RunTestSteps(t, false, func(ctx context.Context) error {
				ctx = td.WithExecRunFn(ctx, commandCollector.Run)

				err := UploadToGold(ctx, utgArgs, "/path/to/skia/bazel-testlogs/some/test/target/test.outputs/outputs.zip")

				assert.NoError(t, err)
				return err
			})

			require.Empty(t, res.Errors)
			require.Empty(t, res.Exceptions)
			testutils.AssertStepNames(t, res,
				"Test did not produce an undeclared test outputs ZIP file or directory; nothing to upload to Gold",
			)

			assert.Empty(t, commandCollector.Commands())
		})
	}

	test("post-submit task", UploadToGoldArgs{
		TestOnlyAllowAnyBazelLabel: true,
		BazelLabel:                 "//some/test:target",
		DeviceSpecificBazelConfig:  "Pixel5",
		GoldctlPath:                "/path/to/goldctl",
		GitCommit:                  "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99",
	})

	test("CL task", UploadToGoldArgs{
		TestOnlyAllowAnyBazelLabel: true,
		BazelLabel:                 "//some/test:target",
		DeviceSpecificBazelConfig:  "Pixel5",
		GoldctlPath:                "/path/to/goldctl",
		GitCommit:                  "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99",

		// These arguments are only used when there are images to upload, and are therefore
		// ignored by the task driver under this test.
		ChangelistID:  "changelist-id",
		PatchsetOrder: "1",
		TryjobID:      "tryjob-id",
	})
}

func TestUploadToGold_WithOutputsZIPOrDir_NoValidImages_NoGoldctlInvocations(t *testing.T) {
	test := func(name string, zip bool, utgArgs UploadToGoldArgs) {
		t.Run(name, func(t *testing.T) {
			undeclaredTestOutputs := map[string]string{
				// The contents of PNG files does not matter for this test.
				"image-with-invalid-json-file.png": "fake PNG",
				"image-with-invalid-json-file.json": `{
					"invalid JSON file": "This JSON file should be ignored"
				}`,
				"image-with-no-json-file.png": "fake PNG",
				"json-file-with-no-image.json": `{
					"md5": "00000000000000000000000000000000",
					"keys": {
						"name": "no-image",
						"source_type": "no-corpus"
					}
				}`,
				"not-an-image-nor-json-file.txt": "I'm neither a PNG nor a JSON file.",
			}

			// Write undeclared test outputs to disk.
			outputsZIPOrDir := ""
			if zip {
				outputsZIPOrDir = filepath.Join(t.TempDir(), "bazel-testlogs", "some", "test", "target", "test.outputs", "outputs.zip")
				require.NoError(t, os.MkdirAll(filepath.Dir(outputsZIPOrDir), 0700))
				testutils.MakeZIP(t, outputsZIPOrDir, undeclaredTestOutputs)
			} else {
				outputsZIPOrDir = t.TempDir()
				testutils.PopulateDir(t, outputsZIPOrDir, undeclaredTestOutputs)
			}

			// Will be returned by the mocked os_steps.TempDir() when the task driver tries to create a
			// directory in which to extract the undeclared outputs ZIP archive.
			outputsZIPExtractionDir := t.TempDir()

			commandCollector := exec.CommandCollector{}
			res := td.RunTestSteps(t, false, func(ctx context.Context) error {
				ctx = td.WithExecRunFn(ctx, commandCollector.Run)

				if zip {
					// Mock os_steps.TempDir() only for the case where outpusZIPOrDir is a ZIP archive. We
					// don't need to assert the exact number of times that os_steps.TempDir() is called
					// because said function produces a "Creating TempDir" task driver step, and we check the
					// exact set of steps produced.
					ctx = context.WithValue(ctx, os_steps.TempDirContextKey, testutils.MakeTempDirMockFn(t, outputsZIPExtractionDir))
				}

				err := UploadToGold(ctx, utgArgs, outputsZIPOrDir)

				assert.NoError(t, err)
				return err
			})

			require.Empty(t, res.Errors)
			require.Empty(t, res.Exceptions)

			expectedSteps := []string{}
			if zip {
				expectedSteps = append(expectedSteps,
					"Creating TempDir",
					"Extract undeclared outputs archive "+outputsZIPOrDir+" into "+outputsZIPExtractionDir,
					"Extracting file: image-with-invalid-json-file.json",
					"Extracting file: image-with-invalid-json-file.png",
					"Extracting file: image-with-no-json-file.png",
					"Extracting file: json-file-with-no-image.json",
					"Not extracting non-PNG / non-JSON file: not-an-image-nor-json-file.txt",
				)
			}
			expectedSteps = append(expectedSteps,
				"Gather JSON and PNG files produced by GMs",
				"Ignoring \"image-with-invalid-json-file.json\": field \"md5\" not found",
				"Ignoring \"json-file-with-no-image.json\": file \"json-file-with-no-image.png\" not found",
				"Undeclared test outputs ZIP file or directory contains no GM outputs; nothing to upload to Gold",
			)
			testutils.AssertStepNames(t, res, expectedSteps...)

			assert.Empty(t, commandCollector.Commands())
		})
	}

	postSubmitTaskArgs := UploadToGoldArgs{
		TestOnlyAllowAnyBazelLabel: true,
		BazelLabel:                 "//some/test:target",
		DeviceSpecificBazelConfig:  "Pixel5",
		GoldctlPath:                "/path/to/goldctl",
		GitCommit:                  "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99",
	}
	test("post-submit task, ZIP file", true /* =zip */, postSubmitTaskArgs)
	test("post-submit task, directory", false /* =zip */, postSubmitTaskArgs)

	clTaskArgs := UploadToGoldArgs{
		TestOnlyAllowAnyBazelLabel: true,
		BazelLabel:                 "//some/test:target",
		DeviceSpecificBazelConfig:  "Pixel5",
		GoldctlPath:                "/path/to/goldctl",
		GitCommit:                  "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99",

		// These arguments are only used when there are images to upload, and are therefore
		// ignored by the task driver under this test.
		ChangelistID:  "changelist-id",
		PatchsetOrder: "1",
		TryjobID:      "tryjob-id",
	}
	test("CL task, ZIP file", true /* =zip */, clTaskArgs)
	test("CL task, directory", false /* =zip */, clTaskArgs)
}

func TestUploadToGold_WithOutputsZIPOrDir_ValidImages_ImagesUploadedToGold(t *testing.T) {
	test := func(name string, zip bool, utgArgs UploadToGoldArgs, goldctlWorkDir, goldctlImgtestInitStepName string, goldctlImgtestInitArgs []string) {
		t.Run(name, func(t *testing.T) {
			undeclaredTestOutputs := map[string]string{
				// The contents of PNG files does not matter for this test.
				"alfa.png": "fake PNG",
				"alfa.json": `{
					"md5": "a01a01a01a01a01a01a01a01a01a01a0",
					"keys": {
						"build_system": "bazel",
						"name": "alfa",
						"source_type": "gm"
					}
				}`,
				"beta.png": "fake PNG",
				"beta.json": `{
					"md5": "b02b02b02b02b02b02b02b02b02b02b0",
					"keys": {
						"build_system": "bazel",
						"name": "beta",
						"source_type": "gm"
					}
				}`,
				"image-with-invalid-json-file.png": "fake PNG",
				"image-with-invalid-json-file.json": `{
					"invalid JSON file": "This JSON file should be ignored"
				}`,
				"image-with-no-json-file.png": "fake PNG",
				"json-file-with-no-image.json": `{
					"md5": "00000000000000000000000000000000",
					"keys": {
						"name": "no-image",
						"source_type": "no-corpus"
					}
				}`,
				"not-an-image-nor-json-file.txt": "I'm neither a PNG nor a JSON file.",
			}

			// Write undeclared test outputs to disk.
			outpusZIPOrDir := ""
			if zip {
				outpusZIPOrDir = filepath.Join(t.TempDir(), "bazel-testlogs", "some", "test", "target", "test.outputs", "outputs.zip")
				require.NoError(t, os.MkdirAll(filepath.Dir(outpusZIPOrDir), 0700))
				testutils.MakeZIP(t, outpusZIPOrDir, undeclaredTestOutputs)
			} else {
				outpusZIPOrDir = t.TempDir()
				testutils.PopulateDir(t, outpusZIPOrDir, undeclaredTestOutputs)
			}

			// Will be returned by the mocked os_steps.TempDir() when the task driver tries to create a
			// directory in which to extract the undeclared outputs ZIP archive.
			outputsZIPExtractionDir := t.TempDir()

			commandCollector := exec.CommandCollector{}
			res := td.RunTestSteps(t, false, func(ctx context.Context) error {
				ctx = td.WithExecRunFn(ctx, commandCollector.Run)

				tempDirMockFn := testutils.MakeTempDirMockFn(t, goldctlWorkDir)
				if zip {
					tempDirMockFn = testutils.MakeTempDirMockFn(t, outputsZIPExtractionDir, goldctlWorkDir)
				}
				// We don't need to assert the exact number of times that os_steps.TempDir() is called
				// because said function produces a "Creating TempDir" task driver step, and we check the
				// exact set of steps produced.
				ctx = context.WithValue(ctx, os_steps.TempDirContextKey, tempDirMockFn)

				err := UploadToGold(ctx, utgArgs, outpusZIPOrDir)

				assert.NoError(t, err)
				return err
			})

			require.Empty(t, res.Errors)
			require.Empty(t, res.Exceptions)

			dirWithTestOutputs := outpusZIPOrDir
			if zip {
				dirWithTestOutputs = outputsZIPExtractionDir
			}

			expectedSteps := []string{}
			if zip {
				expectedSteps = append(expectedSteps,
					"Creating TempDir",
					"Extract undeclared outputs archive "+outpusZIPOrDir+" into "+outputsZIPExtractionDir,
					"Extracting file: alfa.json",
					"Extracting file: alfa.png",
					"Extracting file: beta.json",
					"Extracting file: beta.png",
					"Extracting file: image-with-invalid-json-file.json",
					"Extracting file: image-with-invalid-json-file.png",
					"Extracting file: image-with-no-json-file.png",
					"Extracting file: json-file-with-no-image.json",
					"Not extracting non-PNG / non-JSON file: not-an-image-nor-json-file.txt",
				)
			}
			expectedSteps = append(expectedSteps,
				"Gather JSON and PNG files produced by GMs",
				"Gather \"alfa.png\"",
				"Gather \"beta.png\"",
				"Ignoring \"image-with-invalid-json-file.json\": field \"md5\" not found",
				"Ignoring \"json-file-with-no-image.json\": file \"json-file-with-no-image.png\" not found",
				"Upload GM outputs to Gold",
				"Creating TempDir",
				"/path/to/goldctl auth --work-dir "+goldctlWorkDir+" --luci",
				goldctlImgtestInitStepName,
				"/path/to/goldctl imgtest add --work-dir "+goldctlWorkDir+" --test-name alfa --png-file "+dirWithTestOutputs+"/alfa.png --png-digest a01a01a01a01a01a01a01a01a01a01a0 --add-test-key build_system:bazel --add-test-key name:alfa --add-test-key source_type:gm",
				"/path/to/goldctl imgtest add --work-dir "+goldctlWorkDir+" --test-name beta --png-file "+dirWithTestOutputs+"/beta.png --png-digest b02b02b02b02b02b02b02b02b02b02b0 --add-test-key build_system:bazel --add-test-key name:beta --add-test-key source_type:gm",
				"/path/to/goldctl imgtest finalize --work-dir "+goldctlWorkDir,
			)
			testutils.AssertStepNames(t, res, expectedSteps...)

			exec_testutils.AssertCommandsMatch(t, [][]string{
				{
					"/path/to/goldctl",
					"auth",
					"--work-dir", goldctlWorkDir,
					"--luci",
				},
				append([]string{"/path/to/goldctl"}, goldctlImgtestInitArgs...,
				),
				{
					"/path/to/goldctl",
					"imgtest",
					"add",
					"--work-dir", goldctlWorkDir,
					"--test-name", "alfa",
					"--png-file", dirWithTestOutputs + "/alfa.png",
					"--png-digest", "a01a01a01a01a01a01a01a01a01a01a0",
					"--add-test-key", "build_system:bazel",
					"--add-test-key", "name:alfa",
					"--add-test-key", "source_type:gm",
				},
				{
					"/path/to/goldctl",
					"imgtest",
					"add",
					"--work-dir", goldctlWorkDir,
					"--test-name", "beta",
					"--png-file", dirWithTestOutputs + "/beta.png",
					"--png-digest", "b02b02b02b02b02b02b02b02b02b02b0",
					"--add-test-key", "build_system:bazel",
					"--add-test-key", "name:beta",
					"--add-test-key", "source_type:gm",
				},
				{
					"/path/to/goldctl",
					"imgtest",
					"finalize",
					"--work-dir", goldctlWorkDir,
				},
			}, commandCollector.Commands())

		})
	}

	goldctlWorkDir := t.TempDir()
	postSubmitTaskArgs := UploadToGoldArgs{
		TestOnlyAllowAnyBazelLabel: true,
		BazelLabel:                 "//some/test:target",
		DeviceSpecificBazelConfig:  "Pixel5",
		GoldctlPath:                "/path/to/goldctl",
		GitCommit:                  "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99",
	}
	postSubmitTaskGoldctlImgtestInitStep := "/path/to/goldctl imgtest init --work-dir " + goldctlWorkDir + " --instance skia --url https://gold.skia.org --bucket skia-infra-gm --git_hash ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99 --key arch:arm64 --key model:Pixel5 --key os:Android"
	postSubmitTaskGoldctlImgtestInitArgs := []string{
		"imgtest",
		"init",
		"--work-dir", goldctlWorkDir,
		"--instance", "skia",
		"--url", "https://gold.skia.org",
		"--bucket", "skia-infra-gm",
		"--git_hash", "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99",
		"--key", "arch:arm64",
		"--key", "model:Pixel5",
		"--key", "os:Android",
	}
	test("post-submit task, ZIP file", true /* =zip */, postSubmitTaskArgs, goldctlWorkDir, postSubmitTaskGoldctlImgtestInitStep, postSubmitTaskGoldctlImgtestInitArgs)
	test("post-submit task, directory", false /* =zip */, postSubmitTaskArgs, goldctlWorkDir, postSubmitTaskGoldctlImgtestInitStep, postSubmitTaskGoldctlImgtestInitArgs)

	goldctlWorkDir = t.TempDir()
	clTaskArgs := UploadToGoldArgs{
		TestOnlyAllowAnyBazelLabel: true,
		BazelLabel:                 "//some/test:target",
		DeviceSpecificBazelConfig:  "Pixel5",
		GoldctlPath:                "/path/to/goldctl",
		GitCommit:                  "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99",
		ChangelistID:               "changelist-id",
		PatchsetOrder:              "1",
		TryjobID:                   "tryjob-id",
	}
	clTaskGoldctlImgtestInitStep := "/path/to/goldctl imgtest init --work-dir " + goldctlWorkDir + " --instance skia --url https://gold.skia.org --bucket skia-infra-gm --git_hash ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99 --crs gerrit --cis buildbucket --changelist changelist-id --patchset 1 --jobid tryjob-id --key arch:arm64 --key model:Pixel5 --key os:Android"
	clTaskGoldctlImgtestInitArgs := []string{
		"imgtest",
		"init",
		"--work-dir", goldctlWorkDir,
		"--instance", "skia",
		"--url", "https://gold.skia.org",
		"--bucket", "skia-infra-gm",
		"--git_hash", "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99",
		"--crs", "gerrit",
		"--cis", "buildbucket",
		"--changelist", "changelist-id",
		"--patchset", "1",
		"--jobid", "tryjob-id",
		"--key", "arch:arm64",
		"--key", "model:Pixel5",
		"--key", "os:Android",
	}
	test("CL task, ZIP file", true /* =zip */, clTaskArgs, goldctlWorkDir, clTaskGoldctlImgtestInitStep, clTaskGoldctlImgtestInitArgs)
	test("CL task, directory", false /* =zip */, clTaskArgs, goldctlWorkDir, clTaskGoldctlImgtestInitStep, clTaskGoldctlImgtestInitArgs)
}
