package main

import (
	"os"
	"path"
	"sort"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

const (
	nonTestFile       = "not_test_file.go"
	goTestFileNameOne = "first_go_file_test.go"
	goTestFileNameTwo = "second_go_file_test.go"
)

func TestGetGithubActionMatrixForTests(t *testing.T) {
	t.Run("empty dir with no test cases fails", func(t *testing.T) {
		testingDir := t.TempDir()
		_, err := getGithubActionMatrixForTests(testingDir, "", "", nil)
		assert.Error(t, err)
	})

	t.Run("only test functions are picked up", func(t *testing.T) {
		testingDir := t.TempDir()
		createFileWithTestSuiteAndTests(t, "FeeMiddlewareTestSuite", "TestA", "TestB", testingDir, goTestFileNameOne)

		gh, err := getGithubActionMatrixForTests(testingDir, "", "", nil)
		assert.NoError(t, err)

		expected := GithubActionTestMatrix{
			Include: []TestSuitePair{
				{
					EntryPoint: "TestFeeMiddlewareTestSuite",
					Test:       "TestA",
				},
				{
					EntryPoint: "TestFeeMiddlewareTestSuite",
					Test:       "TestB",
				},
			},
		}
		assertGithubActionTestMatricesEqual(t, expected, gh)
	})

	t.Run("all files are picked up", func(t *testing.T) {
		testingDir := t.TempDir()
		createFileWithTestSuiteAndTests(t, "FeeMiddlewareTestSuite", "TestA", "TestB", testingDir, goTestFileNameOne)
		createFileWithTestSuiteAndTests(t, "TransferTestSuite", "TestC", "TestD", testingDir, goTestFileNameTwo)

		gh, err := getGithubActionMatrixForTests(testingDir, "", "", nil)
		assert.NoError(t, err)

		expected := GithubActionTestMatrix{
			Include: []TestSuitePair{
				{
					EntryPoint: "TestTransferTestSuite",
					Test:       "TestC",
				},
				{
					EntryPoint: "TestFeeMiddlewareTestSuite",
					Test:       "TestA",
				},
				{
					EntryPoint: "TestFeeMiddlewareTestSuite",
					Test:       "TestB",
				},
				{
					EntryPoint: "TestTransferTestSuite",
					Test:       "TestD",
				},
			},
		}

		assertGithubActionTestMatricesEqual(t, expected, gh)
	})

	t.Run("single test can be specified", func(t *testing.T) {
		testingDir := t.TempDir()
		createFileWithTestSuiteAndTests(t, "FeeMiddlewareTestSuite", "TestA", "TestB", testingDir, goTestFileNameOne)
		createFileWithTestSuiteAndTests(t, "TransferTestSuite", "TestC", "TestD", testingDir, goTestFileNameTwo)

		gh, err := getGithubActionMatrixForTests(testingDir, "TestA", "TestFeeMiddlewareTestSuite", nil)
		assert.NoError(t, err)

		expected := GithubActionTestMatrix{
			Include: []TestSuitePair{
				{
					EntryPoint: "TestFeeMiddlewareTestSuite",
					Test:       "TestA",
				},
			},
		}

		assertGithubActionTestMatricesEqual(t, expected, gh)
	})

	t.Run("error if single test doesn't exist", func(t *testing.T) {
		testingDir := t.TempDir()
		createFileWithTestSuiteAndTests(t, "FeeMiddlewareTestSuite", "TestA", "TestB", testingDir, goTestFileNameOne)

		_, err := getGithubActionMatrixForTests(testingDir, "TestThatDoesntExist", "TestFeeMiddlewareTestSuite", nil)
		assert.Error(t, err)
	})

	t.Run("non test files are skipped", func(t *testing.T) {
		testingDir := t.TempDir()
		createFileWithTestSuiteAndTests(t, "FeeMiddlewareTestSuite", "TestA", "TestB", testingDir, nonTestFile)

		gh, err := getGithubActionMatrixForTests(testingDir, "", "", nil)
		assert.Error(t, err)
		assert.Empty(t, gh.Include)
	})

	t.Run("fails when there are multiple suite runs", func(t *testing.T) {
		testingDir := t.TempDir()
		createFileWithTestSuiteAndTests(t, "FeeMiddlewareTestSuite", "TestA", "TestB", testingDir, nonTestFile)

		fileWithTwoSuites := `package foo
func SuiteOne(t *testing.T) {
	suite.Run(t, new(FeeMiddlewareTestSuite))
}

func SuiteTwo(t *testing.T) {
	suite.Run(t, new(FeeMiddlewareTestSuite))
}

type FeeMiddlewareTestSuite struct {}
`

		err := os.WriteFile(path.Join(testingDir, goTestFileNameOne), []byte(fileWithTwoSuites), os.FileMode(0o777))
		assert.NoError(t, err)

		_, err = getGithubActionMatrixForTests(testingDir, "", "", nil)
		assert.Error(t, err)
	})
}

func assertGithubActionTestMatricesEqual(t *testing.T, expected, actual GithubActionTestMatrix) {
	t.Helper()
	// sort by both suite and test as the order of the end result does not matter as
	// all tests will be run.
	sort.SliceStable(expected.Include, func(i, j int) bool {
		memberI := expected.Include[i]
		memberJ := expected.Include[j]
		if memberI.EntryPoint == memberJ.EntryPoint {
			return memberI.Test < memberJ.Test
		}
		return memberI.EntryPoint < memberJ.EntryPoint
	})

	sort.SliceStable(actual.Include, func(i, j int) bool {
		memberI := actual.Include[i]
		memberJ := actual.Include[j]
		if memberI.EntryPoint == memberJ.EntryPoint {
			return memberI.Test < memberJ.Test
		}
		return memberI.EntryPoint < memberJ.EntryPoint
	})
	assert.Equal(t, expected.Include, actual.Include)
}

func goTestFileContents(suiteName, fnName1, fnName2 string) string {
	replacedSuiteName := strings.ReplaceAll(`package foo

func TestSuiteName(t *testing.T) {
	suite.Run(t, new(SuiteName))
}

type SuiteName struct {}

func (s *SuiteName) fnName1() {}
func (s *SuiteName) fnName2() {}

func (s *SuiteName) suiteHelper() {}

func helper() {}
`, "SuiteName", suiteName)

	replacedFn1Name := strings.ReplaceAll(replacedSuiteName, "fnName1", fnName1)
	return strings.ReplaceAll(replacedFn1Name, "fnName2", fnName2)
}

func createFileWithTestSuiteAndTests(t *testing.T, suiteName, fn1Name, fn2Name, dir, filename string) {
	t.Helper()
	goFileContents := goTestFileContents(suiteName, fn1Name, fn2Name)
	err := os.WriteFile(path.Join(dir, filename), []byte(goFileContents), os.FileMode(0o777))
	assert.NoError(t, err)
}
