package pkg

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

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

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 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 TestRunWithUIFatal(t *testing.T) {
	tests := []struct {
		name           string
		settings       Settings
		promptResponse bool
		configure      func(*Config)
		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"},
		},
		{
			name: "clean system config error",
			settings: Settings{
				Cmd:      CommandCleanSystem,
				PkgNames: []string{DUMMY_PKG1},
				Jobs:     NUM_DEFAULT_JOBS,
			},
			configure: func(cfg *Config) {
				parent := must.Content2File(filepath.Join(t.TempDir(), "not-a-directory"), "")
				cfg.SlackpkgConfigDir = filepath.Join(parent, "slackpkg")
			},
			expected: []string{"not-a-directory"},
		},
		{
			name: "clean system blacklist error",
			settings: Settings{
				Cmd:      CommandCleanSystem,
				PkgNames: []string{DUMMY_PKG1},
				Jobs:     NUM_DEFAULT_JOBS,
			},
			configure: func(cfg *Config) {
				cfg.SlackpkgSystemBlacklist = filepath.Join(t.TempDir(), "missing-blacklist")
			},
			expected: []string{"missing-blacklist"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			args := DummyArgs(t)
			if tt.configure != nil {
				tt.configure(&args.Config)
			}
			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,
			})
		})
	}
}

func TestDownloadBuildInstallUsesOutputEnvForBuiltPackage(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.Config.InstallCmd = MOCK_INSTALL + " "

	pMap := sborepo.NewRepoMap(args.Config.RepoPath)
	dg, err := pMap.DependencyGraph([]string{DUMMY_PKG3})
	if err != nil {
		t.Fatal(err)
	}

	err = downloadBuildInstall(dg, args.Config, Settings{
		BuildOptions: map[string][]string{},
		CheckMD5Sum:  true,
		Jobs:         NUM_DEFAULT_JOBS,
	}, func(string, string) {}, cli.Executor{
		Stdout: &mockStdOut,
		Stderr: &mockStdErr,
	})
	if err != nil {
		t.Fatalf("downloadBuildInstall() returned an error: %v", err)
	}

	mustContain(t, mockStdOut.String(), "mocking download")
	if _, err := os.Stat(filepath.Join(args.Config.TmpDir, DUMMY_PKG3_DOWNLOAD)); err != nil {
		t.Fatalf("expected %q in tmpDir: %v", DUMMY_PKG3_DOWNLOAD, err)
	}
	linkPath := filepath.Join(dummyPkg3.Path(), DUMMY_PKG3_DOWNLOAD)
	info, err := os.Lstat(linkPath)
	if err != nil {
		t.Fatalf("expected symlink %q to exist: %v", linkPath, err)
	}
	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(args.Config.TmpDir, DUMMY_PKG3_DOWNLOAD)
	if target != want {
		t.Fatalf("symlink target: got %q, want %q", target, want)
	}
	mustContain(t,
		string(must.ReadFile(filepath.Join(dummyPkg3.Path(), "Install.log"))),
		filepath.Join(outputDir, pkgFile),
	)
}
