package pkg

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"gitlab.com/M0M097/pkg/cli"
	"gitlab.com/M0M097/pkg/internal/must"
	"gitlab.com/M0M097/pkg/sborepo"
)

const runFatalSettingsErrorEnv = "PKG_TEST_RUN_FATAL_SETTINGS_ERROR"

type recordingUI struct {
	t      *testing.T
	alerts []string
}

type fatalUI struct {
	promptResponse bool
	fatalMessage   string
}

type runFatalPanic struct{}

func (r *recordingUI) Alert(msg string) {
	r.alerts = append(r.alerts, msg)
}

func (r *recordingUI) ListLines(lines []string) {
	for _, l := range lines {
		r.Alert(l)
	}
}

func (r *recordingUI) ChoosePkgs(sborepo.PkgIter) sborepo.PkgIter {
	return func(sborepo.PkgFunc) {}
}

func (r *recordingUI) Display(...string) error {
	return nil
}

func (r *recordingUI) Prompt(string) bool {
	return true
}

func (r *recordingUI) NewProgressReporter([]string) cli.ProgressReporter {
	return func(string, string) {}
}

func (r *recordingUI) Fatal(v ...interface{}) {
	r.t.Helper()
	r.t.Fatal(v...)
}

func (f *fatalUI) Alert(string) {}

func (f *fatalUI) ListLines([]string) {}

func (f *fatalUI) ChoosePkgs(sborepo.PkgIter) sborepo.PkgIter {
	return func(sborepo.PkgFunc) {}
}

func (f *fatalUI) Display(...string) error {
	return nil
}

func (f *fatalUI) Prompt(string) bool {
	return f.promptResponse
}

func (f *fatalUI) NewProgressReporter([]string) cli.ProgressReporter {
	return func(string, string) {}
}

func (f *fatalUI) Fatal(v ...interface{}) {
	f.fatalMessage = fmt.Sprint(v...)
	panic(runFatalPanic{})
}

func TestRunWrapper(t *testing.T) {
	var (
		testFlags = []string{VIEW_FLAG, VIEWBUILD_FLAG, SEARCH_FLAG, TRACK_FLAG,
			UPDATE_FLAG, VERSION_FLAG}
		ntests      = len(testFlags)
		output_pkg1 = make([]string, ntests)
		output_pkg2 = make([]string, ntests)
	)
	for i, cmd := range testFlags {
		output_pkg1[i], _, _ = testRunHelperFull(t, "-"+cmd, DUMMY_PKG1, "")
		output_pkg2[i], _, _ = testRunHelperFull(t, "-"+cmd, DUMMY_PKG2, "")
	}

	mustContain(t, output_pkg1[0], "displaying", "README", DUMMY_PKG1)
	mustContain(t, output_pkg2[0], "displaying", "README", DUMMY_PKG2)
	mustContain(t, output_pkg1[1], "displaying", DUMMY_PKG1+".SlackBuild", DUMMY_PKG1)
	mustContain(t, output_pkg2[1], "displaying", DUMMY_PKG2+".SlackBuild", DUMMY_PKG2)
	mustBeEqual(t, output_pkg1[2], DUMMY_PKG1+"\n")
	mustBeEqual(t, output_pkg2[2], DUMMY_PKG2+"\n")
	mustBeEqual(t, output_pkg1[3], DUMMY_PKG2+"\n"+DUMMY_PKG1+"\n")
	mustBeEqual(t, output_pkg2[3], DUMMY_PKG2+"\n")
	_, errPkg1, _ := testRunHelperFull(t, "-"+UPDATE_FLAG, DUMMY_PKG1, "")
	mustContain(t, output_pkg1[4],
		"mocking sync",
		"mocking clean",
		"Space separated list of packages to exclude:",
		DUMMY_PKG2,
		DUMMY_PKG1,
	)
	mustContain(t, errPkg1,
		DUMMY_PKG2+": Failed Build:",
		filepath.Join(DUMMY_PKG2, "Build.log"),
		DUMMY_PKG1+": Skipped; depends on "+DUMMY_PKG2,
	)
	_, errPkg2, _ := testRunHelperFull(t, "-"+UPDATE_FLAG, DUMMY_PKG2, "")
	mustContain(t, output_pkg2[4],
		"mocking sync",
		"mocking clean",
		"Space separated list of packages to exclude:",
		"1 "+DUMMY_PKG2,
	)
	mustContain(t, errPkg2,
		DUMMY_PKG2+": Failed Build:",
		filepath.Join(DUMMY_PKG2, "Build.log"),
	)
	mustNotContain(t, output_pkg2[4], DUMMY_PKG1)
	mustBeEqual(t, output_pkg1[5], VERSION+"\n")
	mustBeEqual(t, output_pkg2[5], VERSION+"\n")

	// Check a successful build and install with dummy download files
	output_pkg3, _, tmpDir := testRunHelperFull(t, "-"+UPDATE_FLAG, DUMMY_PKG3, "")
	/* ------------------------------- checks --------------------------------- */
	// 1. the download branch executed
	mustContain(t, output_pkg3, "mocking download")
	mustContain(t, output_pkg3, "All packages installed successfully")
	mustContain(t, string(must.ReadFile(filepath.Join(dummyPkg3.Path(), "Install.log"))), "mocking install")

	// 2. "downloaded" file exists in tmpDir
	if _, err := os.Stat(filepath.Join(tmpDir, DUMMY_PKG3_DOWNLOAD)); err != nil {
		t.Fatalf("expected %q in tmpDir: %v", DUMMY_PKG3_DOWNLOAD, err)
	}

	// 3. a symlink was created inside the package directory
	linkPath := filepath.Join(dummyPkg3.Path(), DUMMY_PKG3_DOWNLOAD)
	info, err := os.Lstat(linkPath)
	if err != nil {
		t.Fatalf("expected symlink %q to exist: %v\noutput: %s", linkPath, err, output_pkg3)
	}
	if info.Mode()&os.ModeSymlink == 0 {
		t.Fatalf("expected %q to be a symlink", linkPath)
	}
	target, err := os.Readlink(linkPath)
	if err != nil {
		t.Fatalf("readlink failed: %v", err)
	}
	want := filepath.Join(tmpDir, DUMMY_PKG3_DOWNLOAD)
	if target != want {
		t.Fatalf("symlink target: got %q, want %q", target, want)
	}
}

func TestDisplayDependents(t *testing.T) {
	output, _, _ := testRunHelperFull(t, "-"+ROOT_FLAG+" "+DUMMY_PKG2, DUMMY_PKG_FILE_CONTENT, "")
	mustBeEqual(t, output, DUMMY_PKG2+"\n"+DUMMY_PKG1+"\n")
}

func TestNoDeps(t *testing.T) {
	output, _, _ := testRunHelperFull(t, "-"+NO_DEPS_FLAG, DUMMY_PKG1+"\n", "")
	mustContain(t, output, DUMMY_PKG1)
	mustNotContain(t, output, DUMMY_PKG2)
}

func TestSkipPackages(t *testing.T) {
	output, _, _ := testRunHelperFull(t, "-"+UPDATE_FLAG, DUMMY_PKG_FILE_CONTENT, "1 2")
	mustContain(t, output, "All packages skipped")
}

func TestSkipGreylist(t *testing.T) {
	output, stderr, _ := testRunHelperFull(t, "-"+UPDATE_FLAG, DUMMY_GREYLISTED_PKG, "")
	mustContain(t, output, "Nothing to do")
	mustContain(t, stderr, "greylisted:", DUMMY_GREYLISTED_PKG)
}

func TestRunUsesOutputEnvForBuiltPackage(t *testing.T) {
	var (
		args       = DummyArgs(t)
		mockStdOut strings.Builder
		mockStdErr strings.Builder
		outputDir  = t.TempDir()
		pkgFile    = DUMMY_PKG3 + "-" + DUMMY_VERSION + "-noarch-1_SBo.txz"
	)
	oldOutput, hadOutput := os.LookupEnv("OUTPUT")
	os.Setenv("OUTPUT", outputDir)
	defer func() {
		if hadOutput {
			os.Setenv("OUTPUT", oldOutput)
		} else {
			os.Unsetenv("OUTPUT")
		}
	}()

	os.Remove(filepath.Join(args.Config.TmpDir, pkgFile))
	must.Content2File(filepath.Join(outputDir, pkgFile), "")
	args.Streams.Out = &mockStdOut
	args.Streams.Err = &mockStdErr
	args.Argv = []string{"pkg", "-" + YES_FLAG, DUMMY_PKG3}
	args.Config.InstallCmd = MOCK_INSTALL + " "

	Run(args)

	mustContain(t, mockStdOut.String(), "All packages installed successfully")
	mustContain(t,
		string(must.ReadFile(filepath.Join(dummyPkg3.Path(), "Install.log"))),
		filepath.Join(outputDir, pkgFile),
	)
}

func testRunHelperFull(t *testing.T, cmdLine, packages, input string) (string, string, string) {
	var (
		args       = DummyArgs(t)
		mockStdOut strings.Builder
		mockStdErr strings.Builder
	)

	argv := append([]string{"pkg"}, strings.Fields(cmdLine)...)
	must.Content2File(args.Config.DefaultPkgFile, packages)
	args.Streams.Out = &mockStdOut
	args.Streams.In = strings.NewReader(input)
	args.Streams.Err = &mockStdErr
	args.Argv = argv

	PagerCmd := os.Getenv("PAGER")
	os.Setenv("PAGER", "/bin/echo displaying: ")
	defer os.Setenv("PAGER", PagerCmd)

	Run(args)

	return mockStdOut.String(), mockStdErr.String(), args.Config.TmpDir
}

func TestSkipBlacklistUpdate(t *testing.T) {
	var (
		args       = DummyArgs(t)
		mockStdOut strings.Builder
		mockStdErr strings.Builder
	)

	args.Streams.Out = &mockStdOut
	args.Streams.Err = &mockStdErr
	must.Chmod(args.Config.Blacklist, 0o444) // Make it read-only

	Run(args)

	mustContain(t, mockStdErr.String(), "Skipping blacklist update", "open "+args.Config.Blacklist+": permission denied")

}

func TestRunRepoDoesNotExist(t *testing.T) {
	var mockStdOut strings.Builder
	pkgFile := must.Content2File(filepath.Join(t.TempDir(), "pkgfile"), DUMMY_PKG_FILE_CONTENT)
	args := DummyArgs(t)
	args.Streams.Out = &mockStdOut
	args.Config.DefaultPkgFile = pkgFile
	args.Config.RepoPath = filepath.Join(t.TempDir(), "doesnotexist")
	args.Config.PullCmd = MOCK_PULL + " && exit 1"

	defer func() {
		if r := recover(); r == nil {
			t.Error("Expected panic, but got none")
		}
	}()
	Run(args)
	mustContain(t, mockStdOut.String(), "mocking pull")
	mustNotContain(t, mockStdOut.String(), "mocking sync", "mocking clean", "mocking download")
}

func TestListInstalled(t *testing.T) {
	var (
		mockStdOut strings.Builder
		args       = DummyArgs(t)
		docDir     = t.TempDir()
	)
	args.Streams.Out = &mockStdOut
	args.Argv = []string{"pkg", "-" + LIST_INSTALLED_FLAG}
	args.Config.DocDir = docDir
	must.Mkdir(filepath.Join(docDir, DUMMY_PKG1+"-dummy_version"))
	Run(args)
	mustBeEqual(t, mockStdOut.String(), DUMMY_PKG1+"\n")
}

func TestRunWithUIUsesProvidedUI(t *testing.T) {
	var (
		args     = DummyArgs(t)
		stdout   strings.Builder
		customUI = recordingUI{t: t}
	)
	args.Streams.Out = &stdout

	RunWithUI(args.Config, &customUI, Settings{Cmd: CommandShowVersion}, cli.Executor{
		Stdout: args.Streams.Out,
		Stderr: args.Streams.Err,
	})

	mustBeEqualStringSlices(t, customUI.alerts, []string{VERSION})
	mustBeEqual(t, stdout.String(), "")
}

func TestRunFatalSettingsError(t *testing.T) {
	if os.Getenv(runFatalSettingsErrorEnv) == "1" {
		args := DummyArgs(t)
		args.Streams.Out = os.Stdout
		args.Streams.Err = os.Stderr
		args.Argv = []string{"pkg", "-" + JOBS_FLAG, "0"}

		Run(args)
		t.Fatal("Run returned without exiting for invalid settings")
	}

	cmd := exec.Command(os.Args[0], "-test.run=^TestRunFatalSettingsError$")
	cmd.Env = append(os.Environ(), runFatalSettingsErrorEnv+"=1")
	output, err := cmd.CombinedOutput()
	if err == nil {
		t.Fatal("expected Run to exit for invalid settings")
	}
	exitErr, ok := err.(*exec.ExitError)
	if !ok {
		t.Fatalf("expected an exit error for invalid settings, got %T: %v", err, err)
	}
	if exitErr.ExitCode() != 1 {
		t.Fatalf("expected exit code 1 for invalid settings, got %d\nOutput:\n%s",
			exitErr.ExitCode(), output)
	}
	mustContain(t, string(output), "-j must be at least 1, got 0")
}

func TestRunWithUIFatal(t *testing.T) {
	tests := []struct {
		name           string
		settings       Settings
		promptResponse bool
		expected       []string
	}{
		{
			name: "empty dependency graph",
			settings: Settings{
				Cmd:      CommandUpgrade,
				PkgNames: []string{"missing-package"},
				Jobs:     NUM_DEFAULT_JOBS,
			},
			expected: []string{"packages not found in repository", "missing-package"},
		},
		{
			name: "partial dependency graph aborted",
			settings: Settings{
				Cmd:      CommandUpgrade,
				PkgNames: []string{DUMMY_PKG3, "missing-package"},
				Jobs:     NUM_DEFAULT_JOBS,
			},
			expected: []string{"Aborting."},
		},
		{
			name: "view missing package",
			settings: Settings{
				Cmd:      CommandView,
				PkgNames: []string{"missing-package"},
				Jobs:     NUM_DEFAULT_JOBS,
			},
			expected: []string{"packages not found in repository", "missing-package"},
		},
		{
			name: "view build missing package",
			settings: Settings{
				Cmd:      CommandViewBuild,
				PkgNames: []string{"missing-package"},
				Jobs:     NUM_DEFAULT_JOBS,
			},
			expected: []string{"packages not found in repository", "missing-package"},
		},
		{
			name: "search invalid regex",
			settings: Settings{
				Cmd:      CommandSearch,
				PkgNames: []string{"["},
				Jobs:     NUM_DEFAULT_JOBS,
			},
			expected: []string{"error parsing regexp"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Helper()

			args := DummyArgs(t)
			ui := fatalUI{promptResponse: tt.promptResponse}
			defer func() {
				r := recover()
				if r == nil {
					t.Fatalf("expected RunWithUI to call Fatal")
				}
				if _, ok := r.(runFatalPanic); !ok {
					panic(r)
				}
				mustContain(t, ui.fatalMessage, tt.expected...)
			}()

			RunWithUI(args.Config, &ui, tt.settings, cli.Executor{
				Stdout: args.Streams.Out,
				Stderr: args.Streams.Err,
			})
		})
	}
}
