package common_test

import (
	"encoding/json"
	"errors"
	"os"
	"path"
	"path/filepath"
	"testing"
	"time"

	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	. "github.com/smartystreets/goconvey/convey"
	"golang.org/x/crypto/bcrypt"

	tcommon "zotregistry.dev/zot/pkg/test/common"
)

var ErrTestError = errors.New("ErrTestError")

func TestCopyFiles(t *testing.T) {
	Convey("sourceDir does not exist", t, func() {
		err := tcommon.CopyFiles("/path/to/some/unexisting/directory", os.TempDir())
		So(err, ShouldNotBeNil)
	})
	Convey("destDir is a file", t, func() {
		dir := t.TempDir()

		err := tcommon.CopyFiles("../../../test/data", dir)
		So(err, ShouldBeNil)

		err = tcommon.CopyFiles(dir, "/etc/passwd")
		So(err, ShouldNotBeNil)
	})
	Convey("sourceDir does not have read permissions", t, func() {
		dir := t.TempDir()

		err := os.Chmod(dir, 0o300)
		So(err, ShouldBeNil)

		err = tcommon.CopyFiles(dir, os.TempDir())
		So(err, ShouldNotBeNil)
	})
	Convey("sourceDir has a subfolder that does not have read permissions", t, func() {
		dir := t.TempDir()

		sdir := "subdir"
		err := os.Mkdir(path.Join(dir, sdir), 0o300)
		So(err, ShouldBeNil)

		err = tcommon.CopyFiles(dir, os.TempDir())
		So(err, ShouldNotBeNil)
	})
	Convey("sourceDir has a file that does not have read permissions", t, func() {
		dir := t.TempDir()

		filePath := path.Join(dir, "file.txt")

		err := os.WriteFile(filePath, []byte("some dummy file content"), 0o644) //nolint: gosec
		if err != nil {
			panic(err)
		}

		err = os.Chmod(filePath, 0o300)
		So(err, ShouldBeNil)

		err = tcommon.CopyFiles(dir, os.TempDir())
		So(err, ShouldNotBeNil)
	})
	Convey("sourceDir contains a folder starting with invalid characters", t, func() {
		srcDir := t.TempDir()
		dstDir := t.TempDir()

		err := os.MkdirAll(path.Join(srcDir, "_trivy", "db"), 0o755)
		if err != nil {
			panic(err)
		}

		err = os.MkdirAll(path.Join(srcDir, "test-index"), 0o755)
		if err != nil {
			panic(err)
		}

		filePathTrivy := path.Join(srcDir, "_trivy", "db", "trivy.db")

		err = os.WriteFile(filePathTrivy, []byte("some dummy file content"), 0o644) //nolint: gosec
		if err != nil {
			panic(err)
		}

		var index ispec.Index

		content, err := json.Marshal(index)
		if err != nil {
			panic(err)
		}

		err = os.WriteFile(path.Join(srcDir, "test-index", "index.json"), content, 0o644) //nolint: gosec
		if err != nil {
			panic(err)
		}

		err = tcommon.CopyFiles(srcDir, dstDir)
		So(err, ShouldBeNil)

		_, err = os.Stat(path.Join(dstDir, "_trivy", "db", "trivy.db"))
		So(err, ShouldNotBeNil)
		So(os.IsNotExist(err), ShouldBeTrue)

		_, err = os.Stat(path.Join(dstDir, "test-index", "index.json"))
		So(err, ShouldBeNil)
	})
}

func TestCopyFile(t *testing.T) {
	Convey("destFilePath does not exist", t, func() {
		err := tcommon.CopyFile("/path/to/srcFile", "~/path/to/some/unexisting/destDir/file")
		So(err, ShouldNotBeNil)
	})

	Convey("sourceFile does not exist", t, func() {
		err := tcommon.CopyFile("/path/to/some/unexisting/file", path.Join(t.TempDir(), "destFile.txt"))
		So(err, ShouldNotBeNil)
	})
}

func TestReadLogFileAndSearchString(t *testing.T) {
	logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
	if err != nil {
		panic(err)
	}

	logPath := logFile.Name()
	defer os.Remove(logPath)

	Convey("Invalid path", t, func() {
		_, err = tcommon.ReadLogFileAndSearchString("invalidPath",
			"cve-db update completed, next update scheduled after interval", 1*time.Second)
		So(err, ShouldNotBeNil)
	})

	Convey("Time too short", t, func() {
		ok, err := tcommon.ReadLogFileAndSearchString(logPath, "invalid string", time.Microsecond)
		So(err, ShouldBeNil)
		So(ok, ShouldBeFalse)
	})
}

func TestReadLogFileAndCountStringOccurence(t *testing.T) {
	logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
	if err != nil {
		panic(err)
	}

	_, err = logFile.WriteString("line1\n line2\n line3 line1 line2\n line1")
	if err != nil {
		panic(err)
	}

	logPath := logFile.Name()
	defer os.Remove(logPath)

	Convey("Invalid path", t, func() {
		_, err = tcommon.ReadLogFileAndCountStringOccurence("invalidPath",
			"cve-db update completed, next update scheduled after interval", 1*time.Second, 1)
		So(err, ShouldNotBeNil)
	})

	Convey("Time too short", t, func() {
		ok, err := tcommon.ReadLogFileAndCountStringOccurence(logPath, "invalid string", time.Microsecond, 1)
		So(err, ShouldBeNil)
		So(ok, ShouldBeFalse)
	})

	Convey("Count occurrence working", t, func() {
		ok, err := tcommon.ReadLogFileAndCountStringOccurence(logPath, "line1", 90*time.Second, 3)
		So(err, ShouldBeNil)
		So(ok, ShouldBeTrue)
	})
}

func TestCopyTestKeysAndCerts(t *testing.T) {
	Convey("CopyTestKeysAndCerts", t, func() {
		// ------- Make test files unreadable -------
		dir := t.TempDir()
		file := filepath.Join(dir, "ca.crt")

		_, err := os.Create(file)
		So(err, ShouldBeNil)

		err = os.Chmod(file, 0o000)
		So(err, ShouldBeNil)

		err = tcommon.CopyTestKeysAndCerts(dir)
		So(err, ShouldNotBeNil)

		err = os.Chmod(file, 0o777)
		So(err, ShouldBeNil)

		// ------- Copy fails -------

		err = os.Chmod(dir, 0o000)
		So(err, ShouldBeNil)

		err = tcommon.CopyTestKeysAndCerts(file)
		So(err, ShouldNotBeNil)

		err = os.Chmod(dir, 0o777)
		So(err, ShouldBeNil)

		// ------- Folder creation fails -------

		file = filepath.Join(dir, "a-file.file")
		_, err = os.Create(file)
		So(err, ShouldBeNil)

		_, err = os.Stat(file)
		So(err, ShouldBeNil)

		err = tcommon.CopyTestKeysAndCerts(file)
		So(err, ShouldNotBeNil)

		// ----- /test/data doesn't exist ------
		workDir, err := os.Getwd()
		So(err, ShouldBeNil)

		defer func() { _ = os.Chdir(workDir) }()

		dir = t.TempDir()
		file = filepath.Join(dir, "go.mod")
		_, err = os.Create(file)
		So(err, ShouldBeNil)
		_, err = os.Stat(file)
		So(err, ShouldBeNil)
		err = os.Chdir(dir)
		So(err, ShouldBeNil)
		err = tcommon.CopyTestKeysAndCerts(dir)
		So(err, ShouldNotBeNil)
		So(err.Error(), ShouldContainSubstring, "CopyFiles os.Stat failed")

		// --- GetProjectRootDir call fails -----
		err = os.Chdir(os.TempDir())
		So(err, ShouldBeNil)
		err = tcommon.CopyTestKeysAndCerts(os.TempDir())
		So(err, ShouldNotBeNil)
		So(err, ShouldEqual, tcommon.ErrNoGoModFileFound)
	})
}

func TestGetProjectRootDir(t *testing.T) {
	Convey("GetProjectRootDir", t, func() {
		path, err := tcommon.GetProjectRootDir()
		So(err, ShouldBeNil)
		So(len(path), ShouldBeGreaterThan, 0)
	})
	Convey("GetProjectRootDir negative testing", t, func() {
		workDir, err := os.Getwd()
		So(err, ShouldBeNil)

		defer func() { _ = os.Chdir(workDir) }()

		err = os.Chdir(os.TempDir())
		So(err, ShouldBeNil)
		path, err := tcommon.GetProjectRootDir()
		So(err, ShouldNotBeNil)
		So(err, ShouldEqual, tcommon.ErrNoGoModFileFound)
		So(path, ShouldBeZeroValue)
	})
}

func TestGetCredString(t *testing.T) {
	Convey("GetCredString panics", t, func() {
		passwordSize := 100
		pass := make([]byte, passwordSize)

		for i := 0; i < passwordSize; i++ {
			pass[i] = 'Y'
		}

		f := func() { tcommon.GetCredString("testUser", string(pass)) }
		So(f, ShouldPanicWith, bcrypt.ErrPasswordTooLong)
	})
}
