package pkg

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

	"gitlab.com/M0M097/pkg/sborepo"
)

const (
	benchCategoryCount       = 32
	benchPackagesPerCategory = 48
	benchLayerWidth          = 64
	benchRootCount           = 64
	benchVersion             = "1.0"
	benchSlackBuildContent   = "#!/bin/sh\nexit 0\n"
)

var benchSink interface{}

type syntheticRunFixture struct {
	repoDir        string
	docDir         string
	tmpDir         string
	configDir      string
	greylist       string
	blacklist      string
	otherBlacklist string
	pkgFile        string
	defaultPkgFile string
	packageNames   []string
	rootNames      []string
}

func BenchmarkNewRepoMapSynthetic(b *testing.B) {
	fixture := newSyntheticRunFixture(b)
	b.ReportAllocs()
	b.ReportMetric(float64(len(fixture.packageNames)), "packages")
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		benchSink = sborepo.NewRepoMap(fixture.repoDir)
	}
}

func BenchmarkRunSynthetic(b *testing.B) {
	fixture := newSyntheticRunFixture(b)

	benchmarks := []struct {
		name           string
		args           []string
		defaultPkgFile string
	}{
		{
			name:           "list_installed",
			args:           []string{"pkg", "-" + LIST_INSTALLED_FLAG},
			defaultPkgFile: fixture.defaultPkgFile,
		},
		{
			name:           "search",
			args:           []string{"pkg", "-" + SEARCH_FLAG, "pkg15"},
			defaultPkgFile: fixture.defaultPkgFile,
		},
		{
			name: "dependents",
			args: []string{
				"pkg",
				"-" + ROOT_FLAG, fixture.rootNames[0],
				"-" + FILE_FLAG, fixture.pkgFile,
			},
			defaultPkgFile: fixture.defaultPkgFile,
		},
		{
			name:           "upgrade_up_to_date_custom_file",
			args:           []string{"pkg", "-" + YES_FLAG, "-" + FILE_FLAG, fixture.pkgFile},
			defaultPkgFile: fixture.defaultPkgFile,
		},
		{
			name:           "upgrade_up_to_date_no_deps",
			args:           []string{"pkg", "-" + YES_FLAG, "-" + NO_DEPS_FLAG, "-" + FILE_FLAG, fixture.pkgFile},
			defaultPkgFile: fixture.defaultPkgFile,
		},
		{
			name:           "upgrade_up_to_date_default_file",
			args:           []string{"pkg", "-" + YES_FLAG},
			defaultPkgFile: fixture.pkgFile,
		},
	}

	for _, bm := range benchmarks {
		bm := bm
		b.Run(bm.name, func(b *testing.B) {
			b.ReportAllocs()
			b.ReportMetric(float64(len(fixture.packageNames)), "packages")
			b.ReportMetric(float64(len(fixture.rootNames)), "roots")
			b.ResetTimer()

			for i := 0; i < b.N; i++ {
				benchRun(fixture, bm.args, bm.defaultPkgFile)
			}
		})
	}
}

func benchRun(fixture *syntheticRunFixture, args []string, defaultPkgFile string) {
	Run(RunArgs{
		Streams: Streams{
			Out: io.Discard,
			In:  strings.NewReader(""),
			Err: io.Discard,
		},
		Argv: args,
		Config: Config{
			RepoPath:             fixture.repoDir,
			DocDir:               fixture.docDir,
			TmpDir:               fixture.tmpDir,
			ConfigDir:            fixture.configDir,
			Greylist:             fixture.greylist,
			Blacklist:            fixture.blacklist,
			OtherBlacklist:       fixture.otherBlacklist,
			DefaultPkgFile:       defaultPkgFile,
			SlackwareVersionFile: "",
			PullCmd:              "true",
			SyncCmd:              "true",
			CleanCmd:             "true",
			DownloadCmd:          "true",
			InstallCmd:           "true ",
			FallbackPager:        "true",
		},
	})
}

func newSyntheticRunFixture(b *testing.B) *syntheticRunFixture {
	b.Helper()

	baseDir := b.TempDir()
	fixture := &syntheticRunFixture{
		repoDir:        filepath.Join(baseDir, "repo"),
		docDir:         filepath.Join(baseDir, "doc"),
		tmpDir:         filepath.Join(baseDir, "tmp"),
		configDir:      filepath.Join(baseDir, "config"),
		greylist:       filepath.Join(baseDir, "config", "greylist"),
		blacklist:      filepath.Join(baseDir, "config", "blacklist"),
		otherBlacklist: filepath.Join(baseDir, "config", "other_blacklist"),
		pkgFile:        filepath.Join(baseDir, "config", "pkg_list.txt"),
		defaultPkgFile: filepath.Join(baseDir, "config", "unused_default_pkg_list.txt"),
	}

	benchMkdirAll(b, fixture.repoDir)
	benchMkdirAll(b, fixture.docDir)
	benchMkdirAll(b, fixture.tmpDir)
	benchMkdirAll(b, fixture.configDir)
	benchWriteFile(b, fixture.greylist, "")
	benchWriteFile(b, fixture.blacklist, "baseline\n")
	benchWriteFile(b, fixture.otherBlacklist, "baseline\n")

	totalPackages := benchCategoryCount * benchPackagesPerCategory
	fixture.packageNames = make([]string, totalPackages)
	for i := range fixture.packageNames {
		fixture.packageNames[i] = fmt.Sprintf("pkg%04d", i)
	}

	for category := 0; category < benchCategoryCount; category++ {
		categoryDir := filepath.Join(fixture.repoDir, fmt.Sprintf("category%02d", category))
		benchMkdirAll(b, categoryDir)
		for offset := 0; offset < benchPackagesPerCategory; offset++ {
			index := category*benchPackagesPerCategory + offset
			name := fixture.packageNames[index]
			benchCreatePackage(
				b,
				categoryDir,
				fixture.docDir,
				name,
				benchRequires(fixture.packageNames, index),
			)
		}
	}

	fixture.rootNames = append([]string(nil), fixture.packageNames[totalPackages-benchRootCount:]...)
	pkgFileContent := strings.Join(fixture.rootNames, "\n") + "\n"
	benchWriteFile(b, fixture.pkgFile, pkgFileContent)
	benchWriteFile(b, fixture.defaultPkgFile, pkgFileContent)

	return fixture
}

func benchRequires(names []string, index int) string {
	if index < benchLayerWidth {
		return ""
	}

	deps := []string{names[index-benchLayerWidth]}
	if index%2 == 0 {
		deps = append(deps, names[index-(benchLayerWidth/2)])
	}
	if index >= 2*benchLayerWidth && index%5 == 0 {
		deps = append(deps, names[index-(2*benchLayerWidth)])
	}

	return strings.Join(deps, " ")
}

func benchCreatePackage(b *testing.B, categoryDir, docDir, name, requires string) {
	b.Helper()

	pkgDir := filepath.Join(categoryDir, name)
	benchMkdirAll(b, pkgDir)
	benchWriteFile(b, filepath.Join(pkgDir, name+".SlackBuild"), benchSlackBuildContent)
	benchWriteFile(b, filepath.Join(pkgDir, "README"), "synthetic benchmark package\n")
	benchWriteFile(b, filepath.Join(pkgDir, "slack-desc"), name+": synthetic benchmark package\n")
	benchWriteFile(b, filepath.Join(pkgDir, name+".info"), fmt.Sprintf(`PRGNAM="%s"
VERSION="%s"
HOMEPAGE="https://example.invalid/%s"
DOWNLOAD="https://example.invalid/%s.tar.gz"
MD5SUM="d41d8cd98f00b204e9800998ecf8427e"
DOWNLOAD_x86_64=""
MD5SUM_x86_64=""
REQUIRES="%s"
MAINTAINER="Benchmark"
EMAIL="benchmark@example.invalid"
`, name, benchVersion, name, name, requires))

	docPkgDir := filepath.Join(docDir, name+"-"+benchVersion)
	benchMkdirAll(b, docPkgDir)
	benchWriteFile(b, filepath.Join(docPkgDir, name+".SlackBuild"), benchSlackBuildContent)
}

func benchMkdirAll(b *testing.B, path string) {
	b.Helper()
	if err := os.MkdirAll(path, 0755); err != nil {
		b.Fatalf("mkdir %s: %v", path, err)
	}
}

func benchWriteFile(b *testing.B, path, content string) {
	b.Helper()
	if err := os.WriteFile(path, []byte(content), 0644); err != nil {
		b.Fatalf("write %s: %v", path, err)
	}
}
