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

package exporter

import (
	"bytes"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"go.skia.org/skia/bazel/exporter/build_proto/build"
	"go.skia.org/skia/bazel/exporter/interfaces/mocks"
	"google.golang.org/protobuf/proto"
)

// The expected gn/core.gni file contents for createCoreSourcesQueryResult().
// This expected result is handmade.
const publicSrcsExpectedGNI = `# DO NOT EDIT: This is a generated file.
# See //bazel/exporter_tool/README.md for more information.
#
# The sources of truth are:
#   //src/core/BUILD.bazel
#   //src/opts/BUILD.bazel

# To update this file, run make -C bazel generate_gni

_src = get_path_info("../src", "abspath")

# List generated by Bazel rules:
#  //src/core:core_srcs
#  //src/opts:private_hdrs
skia_core_sources = [
  "$_src/core/SkAAClip.cpp",
  "$_src/core/SkATrace.cpp",
  "$_src/core/SkAlphaRuns.cpp",
  "$_src/opts/SkBitmapProcState_opts.h",
  "$_src/opts/SkBlitMask_opts.h",
  "$_src/opts/SkBlitRow_opts.h",
]

`

var exportDescs = []GNIExportDesc{
	{GNI: "gn/core.gni", Vars: []GNIFileListExportDesc{
		{Var: "skia_core_sources",
			Rules: []string{
				"//src/core:core_srcs",
				"//src/opts:private_hdrs",
			}}},
	},
}

var testExporterParams = GNIExporterParams{
	WorkspaceDir: "/path/to/workspace",
	ExportDescs:  exportDescs,
}

func createCoreSourcesQueryResult() *build.QueryResult {
	qr := build.QueryResult{}
	ruleDesc := build.Target_RULE

	// Rule #1
	srcs := []string{
		"//src/core:SkAAClip.cpp",
		"//src/core:SkATrace.cpp",
		"//src/core:SkAlphaRuns.cpp",
	}
	r1 := createTestBuildRule("//src/core:core_srcs", "filegroup",
		"/path/to/workspace/src/core/BUILD.bazel:376:20", srcs)
	t1 := build.Target{Rule: r1, Type: &ruleDesc}
	qr.Target = append(qr.Target, &t1)

	// Rule #2
	srcs = []string{
		"//src/opts:SkBitmapProcState_opts.h",
		"//src/opts:SkBlitMask_opts.h",
		"//src/opts:SkBlitRow_opts.h",
	}
	r2 := createTestBuildRule("//src/opts:private_hdrs", "filegroup",
		"/path/to/workspace/src/opts/BUILD.bazel:26:10", srcs)
	t2 := build.Target{Rule: r2, Type: &ruleDesc}
	qr.Target = append(qr.Target, &t2)
	return &qr
}

func TestGNIExporterExport_ValidInput_Success(t *testing.T) {
	qr := createCoreSourcesQueryResult()
	protoData, err := proto.Marshal(qr)
	require.NoError(t, err)

	fs := mocks.NewFileSystem(t)
	var contents bytes.Buffer
	fs.On("OpenFile", mock.Anything).Once().Run(func(args mock.Arguments) {
		path := args.String(0)
		assert.True(t, filepath.IsAbs(path))
		assert.Equal(t, "/path/to/workspace/gn/core.gni", filepath.ToSlash(path))
	}).Return(&contents, nil).Once()
	e := NewGNIExporter(testExporterParams, fs)
	qcmd := mocks.NewQueryCommand(t)
	qcmd.On("Read", mock.Anything).Return(protoData, nil).Once()
	err = e.Export(qcmd)
	require.NoError(t, err)

	assert.Equal(t, publicSrcsExpectedGNI, contents.String())
}

func TestMakeRelativeFilePathForGNI_MatchingRootDir_Success(t *testing.T) {
	test := func(name, target, expectedPath string) {
		t.Run(name, func(t *testing.T) {
			path, err := makeRelativeFilePathForGNI(target)
			require.NoError(t, err)
			assert.Equal(t, expectedPath, path)
		})
	}

	test("src", "src/core/file.cpp", "$_src/core/file.cpp")
	test("include", "include/core/file.h", "$_include/core/file.h")
	test("modules", "modules/mod/file.cpp", "$_modules/mod/file.cpp")
}

func TestMakeRelativeFilePathForGNI_IndalidInput_ReturnError(t *testing.T) {
	test := func(name, target string) {
		t.Run(name, func(t *testing.T) {
			_, err := makeRelativeFilePathForGNI(target)
			assert.Error(t, err)
		})
	}

	test("EmptyString", "")
	test("UnsupportedRootDir", "//valid/rule/incorrect/root/dir:file.cpp")
}

func TestIsHeaderFile_HeaderFiles_ReturnTrue(t *testing.T) {
	test := func(name, path string) {
		t.Run(name, func(t *testing.T) {
			assert.True(t, isHeaderFile(path))
		})
	}

	test("LowerH", "path/to/file.h")
	test("UpperH", "path/to/file.H")
	test("MixedHpp", "path/to/file.Hpp")
}

func TestIsHeaderFile_NonHeaderFiles_ReturnTrue(t *testing.T) {
	test := func(name, path string) {
		t.Run(name, func(t *testing.T) {
			assert.False(t, isHeaderFile(path))
		})
	}

	test("EmptyString", "")
	test("DirPath", "/path/to/dir")
	test("C++Source", "/path/to/file.cpp")
	test("DotHInDir", "/path/to/dir.h/file.cpp")
	test("Go", "main.go")
}

func TestFileListContainsOnlyCppHeaderFiles_AllHeaders_ReturnsTrue(t *testing.T) {
	test := func(name string, paths []string) {
		t.Run(name, func(t *testing.T) {
			assert.True(t, fileListContainsOnlyCppHeaderFiles(paths))
		})
	}

	test("OneFile", []string{"file.h"})
	test("Multiple", []string{"file.h", "foo.hpp"})
}

func TestFileListContainsOnlyCppHeaderFiles_NotAllHeaders_ReturnsFalse(t *testing.T) {
	test := func(name string, paths []string) {
		t.Run(name, func(t *testing.T) {
			assert.False(t, fileListContainsOnlyCppHeaderFiles(paths))
		})
	}

	test("Nil", nil)
	test("HeaderFiles", []string{"file.h", "file2.cpp"})
	test("GoFile", []string{"file.go"})
}

func TestWorkspaceToAbsPath_ReturnsAbsolutePath(t *testing.T) {
	fs := mocks.NewFileSystem(t)
	e := NewGNIExporter(testExporterParams, fs)
	require.NotNil(t, e)

	test := func(name, input, expected string) {
		t.Run(name, func(t *testing.T) {
			assert.Equal(t, expected, e.workspaceToAbsPath(input))
		})
	}

	test("FileInDir", "foo/bar.txt", "/path/to/workspace/foo/bar.txt")
	test("DirInDir", "foo/bar", "/path/to/workspace/foo/bar")
	test("RootFile", "root.txt", "/path/to/workspace/root.txt")
	test("WorkspaceDir", "", "/path/to/workspace")
}

func TestAbsToWorkspacePath_PathInWorkspace_ReturnsRelativePath(t *testing.T) {
	fs := mocks.NewFileSystem(t)
	e := NewGNIExporter(testExporterParams, fs)
	require.NotNil(t, e)

	test := func(name, input, expected string) {
		t.Run(name, func(t *testing.T) {
			path, err := e.absToWorkspacePath(input)
			assert.NoError(t, err)
			assert.Equal(t, expected, path)
		})
	}

	test("FileInDir", "/path/to/workspace/foo/bar.txt", "foo/bar.txt")
	test("DirInDir", "/path/to/workspace/foo/bar", "foo/bar")
	test("RootFile", "/path/to/workspace/root.txt", "root.txt")
	test("RootFile", "/path/to/workspace/世界", "世界")
	test("WorkspaceDir", "/path/to/workspace", "")
}

func TestAbsToWorkspacePath_PathNotInWorkspace_ReturnsError(t *testing.T) {
	fs := mocks.NewFileSystem(t)
	e := NewGNIExporter(testExporterParams, fs)
	require.NotNil(t, e)

	_, err := e.absToWorkspacePath("/path/to/file.txt")
	assert.Error(t, err)
}

func TestGetGNILineVariable_LinesWithVariables_ReturnVariable(t *testing.T) {
	test := func(name, inputLine, expected string) {
		t.Run(name, func(t *testing.T) {
			assert.Equal(t, expected, getGNILineVariable(inputLine))
		})
	}

	test("EqualWithSpaces", `foo = [ "something" ]`, "foo")
	test("EqualNoSpaces", `foo=[ "something" ]`, "foo")
	test("EqualSpaceBefore", `foo =[ "something" ]`, "foo")
	test("MultilineList", `foo = [`, "foo")
}

func TestGetGNILineVariable_LinesWithVariables_NoMatch(t *testing.T) {
	test := func(name, inputLine, expected string) {
		t.Run(name, func(t *testing.T) {
			assert.Equal(t, expected, getGNILineVariable(inputLine))
		})
	}

	test("FirstCharSpace", ` foo = [ "something" ]`, "") // Impl. requires formatted file.
	test("NotList", `foo = bar`, "")
	test("ListLiteral", `[ "something" ]`, "")
	test("ListInComment", `# foo = [ "something" ]`, "")
	test("MissingVariable", `=[ "something" ]`, "")
	test("EmptyString", ``, "")
}

func TestExtractTopLevelFolder_PathsWithTopDir_ReturnsTopDir(t *testing.T) {
	test := func(name, input, expected string) {
		t.Run(name, func(t *testing.T) {
			assert.Equal(t, expected, extractTopLevelFolder(input))
		})
	}
	test("TopIsDir", "foo/bar/baz.txt", "foo")
	test("TopIsVariable", "$_src/bar/baz.txt", "$_src")
	test("TopIsFile", "baz.txt", "baz.txt")
	test("TopIsAbsDir", "/foo/bar/baz.txt", "")
}

func TestExtractTopLevelFolder_PathsWithNoTopDir_ReturnsEmptyString(t *testing.T) {
	test := func(name, input, expected string) {
		t.Run(name, func(t *testing.T) {
			assert.Equal(t, expected, extractTopLevelFolder(input))
		})
	}
	test("EmptyString", "", "")
	test("EmptyAbsRoot", "/", "")
	test("MultipleSlashes", "///", "")
}

func TestAddGNIVariablesToWorkspacePaths_ValidInput_ReturnsVariables(t *testing.T) {
	test := func(name string, inputPaths, expected []string) {
		t.Run(name, func(t *testing.T) {
			gniPaths, err := addGNIVariablesToWorkspacePaths(inputPaths)
			require.NoError(t, err)
			assert.Equal(t, expected, gniPaths)
		})
	}
	test("EmptySlice", nil, []string{})
	test("AllVariables",
		[]string{"src/include/foo.h",
			"include/foo.h",
			"modules/foo.cpp"},
		[]string{"$_src/include/foo.h",
			"$_include/foo.h",
			"$_modules/foo.cpp"})
}

func TestAddGNIVariablesToWorkspacePaths_InvalidInput_ReturnsError(t *testing.T) {
	test := func(name string, inputPaths []string) {
		t.Run(name, func(t *testing.T) {
			_, err := addGNIVariablesToWorkspacePaths(inputPaths)
			assert.Error(t, err)
		})
	}
	test("InvalidTopDir", []string{"nomatch/include/foo.h"})
	test("RuleNotPath", []string{"//src/core:source.cpp"})
}

func TestConvertTargetsToFilePaths_ValidInput_ReturnsPaths(t *testing.T) {
	test := func(name string, inputTargets, expected []string) {
		t.Run(name, func(t *testing.T) {
			paths, err := convertTargetsToFilePaths(inputTargets)
			require.NoError(t, err)
			assert.Equal(t, expected, paths)
		})
	}
	test("EmptySlice", nil, []string{})
	test("Files",
		[]string{"//src/include:foo.h",
			"//include:foo.h",
			"//modules:foo.cpp"},
		[]string{"src/include/foo.h",
			"include/foo.h",
			"modules/foo.cpp"})
}

func TestConvertTargetsToFilePaths_InvalidInput_ReturnsError(t *testing.T) {
	test := func(name string, inputTargets []string) {
		t.Run(name, func(t *testing.T) {
			_, err := convertTargetsToFilePaths(inputTargets)
			assert.Error(t, err)
		})
	}
	test("EmptyString", []string{""})
	test("ValidTargetEmptyString", []string{"//src/include:foo.h", ""})
	test("EmptyStringValidTarget", []string{"//src/include:foo.h", ""})
}

func TestRemoveDuplicate_ContainsDuplicates_SortedAndDuplicatesRemoved(t *testing.T) {
	files := []string{
		"alpha",
		"beta",
		"gamma",
		"delta",
		"beta",
		"Alpha",
		"alpha",
		"path/to/file",
		"path/to/file2",
		"path/to/file",
	}
	output := removeDuplicates(files)
	assert.Equal(t, []string{
		"Alpha",
		"alpha",
		"beta",
		"delta",
		"gamma",
		"path/to/file",
		"path/to/file2",
	}, output)
}

func TestRemoveDuplicates_NoDuplicates_ReturnsOnlySorted(t *testing.T) {
	files := []string{
		"Beta",
		"ALPHA",
		"gamma",
		"path/to/file2",
		"path/to/file",
	}
	output := removeDuplicates(files)
	assert.Equal(t, []string{
		"ALPHA",
		"Beta",
		"gamma",
		"path/to/file",
		"path/to/file2",
	}, output)
}

func TestGetPathToTopDir_ValidRelativePaths_ReturnsExpected(t *testing.T) {
	test := func(name, expected, input string) {
		t.Run(name, func(t *testing.T) {
			assert.Equal(t, expected, getPathToTopDir(input))
		})
	}
	test("TopDir", ".", "core.gni")
	test("OneDown", "..", "gn/core.gni")
	test("TwoDown", "../..", "modules/skcms/skcms.gni")
}

func TestGetPathToTopDir_AbsolutePath_ReturnsEmptyString(t *testing.T) {
	// Exporter shouldn't use absolute paths, but just to be safe.
	assert.Equal(t, "", getPathToTopDir("/"))
}
