package matugen

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

	mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
	"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
	"github.com/stretchr/testify/assert"
)

func TestAppendConfigBinaryExists(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	testConfig := "test config content"
	configPath := filepath.Join(configsDir, "test.toml")
	if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	mockChecker := mocks_utils.NewMockAppChecker(t)
	mockChecker.EXPECT().AnyCommandExists("sh").Return(true)

	opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}

	appendConfig(opts, cfgFile, []string{"sh"}, nil, nil, "test.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	if len(output) == 0 {
		t.Errorf("expected config to be written when binary exists")
	}
	if string(output) != testConfig+"\n" {
		t.Errorf("expected %q, got %q", testConfig+"\n", string(output))
	}
}

func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	testConfig := "test config content"
	configPath := filepath.Join(configsDir, "test.toml")
	if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	mockChecker := mocks_utils.NewMockAppChecker(t)
	mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
	mockChecker.EXPECT().AnyFlatpakExists().Return(false)

	opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}

	appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, nil, "test.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	if len(output) != 0 {
		t.Errorf("expected no config when binary doesn't exist, got: %q", string(output))
	}
}

func TestAppendConfigFlatpakExists(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	testConfig := "zen config content"
	configPath := filepath.Join(configsDir, "test.toml")
	if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	mockChecker := mocks_utils.NewMockAppChecker(t)
	mockChecker.EXPECT().AnyFlatpakExists("app.zen_browser.zen").Return(true)

	opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}

	appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, nil, "test.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	if len(output) == 0 {
		t.Errorf("expected config to be written when flatpak exists")
	}
}

func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	testConfig := "test config content"
	configPath := filepath.Join(configsDir, "test.toml")
	if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	mockChecker := mocks_utils.NewMockAppChecker(t)
	mockChecker.EXPECT().AnyCommandExists().Return(false)
	mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)

	opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}

	appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, nil, "test.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	if len(output) != 0 {
		t.Errorf("expected no config when flatpak doesn't exist, got: %q", string(output))
	}
}

func TestAppendConfigBothExist(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	testConfig := "zen config content"
	configPath := filepath.Join(configsDir, "test.toml")
	if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	mockChecker := mocks_utils.NewMockAppChecker(t)
	mockChecker.EXPECT().AnyCommandExists("sh").Return(true)

	opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}

	appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, nil, "test.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	if len(output) == 0 {
		t.Errorf("expected config to be written when both binary and flatpak exist")
	}
}

func TestAppendConfigNeitherExists(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	testConfig := "test config content"
	configPath := filepath.Join(configsDir, "test.toml")
	if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	mockChecker := mocks_utils.NewMockAppChecker(t)
	mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
	mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)

	opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}

	appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, nil, "test.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	if len(output) != 0 {
		t.Errorf("expected no config when neither exists, got: %q", string(output))
	}
}

func TestAppendConfigNoChecks(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	testConfig := "always include"
	configPath := filepath.Join(configsDir, "test.toml")
	if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	opts := &Options{ShellDir: shellDir}

	appendConfig(opts, cfgFile, nil, nil, nil, "test.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	if len(output) == 0 {
		t.Errorf("expected config to be written when no checks specified")
	}
}

func TestAppendConfigFileDoesNotExist(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	opts := &Options{ShellDir: shellDir}

	appendConfig(opts, cfgFile, nil, nil, nil, "nonexistent.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	if len(output) != 0 {
		t.Errorf("expected no config when file doesn't exist, got: %q", string(output))
	}
}

func TestSubstituteVars(t *testing.T) {
	configDir := utils.XDGConfigHome()
	dataDir := utils.XDGDataHome()
	cacheDir := utils.XDGCacheHome()

	tests := []struct {
		name     string
		input    string
		shellDir string
		expected string
	}{
		{
			name:     "substitutes SHELL_DIR",
			input:    "input_path = 'SHELL_DIR/matugen/templates/foo.conf'",
			shellDir: "/home/user/shell",
			expected: "input_path = '/home/user/shell/matugen/templates/foo.conf'",
		},
		{
			name:     "substitutes CONFIG_DIR",
			input:    "output_path = 'CONFIG_DIR/kitty/theme.conf'",
			shellDir: "/home/user/shell",
			expected: "output_path = '" + configDir + "/kitty/theme.conf'",
		},
		{
			name:     "substitutes DATA_DIR",
			input:    "output_path = 'DATA_DIR/color-schemes/theme.colors'",
			shellDir: "/home/user/shell",
			expected: "output_path = '" + dataDir + "/color-schemes/theme.colors'",
		},
		{
			name:     "substitutes CACHE_DIR",
			input:    "output_path = 'CACHE_DIR/wal/colors.json'",
			shellDir: "/home/user/shell",
			expected: "output_path = '" + cacheDir + "/wal/colors.json'",
		},
		{
			name:     "substitutes all dir types",
			input:    "'SHELL_DIR/a' 'CONFIG_DIR/b' 'DATA_DIR/c' 'CACHE_DIR/d'",
			shellDir: "/shell",
			expected: "'/shell/a' '" + configDir + "/b' '" + dataDir + "/c' '" + cacheDir + "/d'",
		},
		{
			name:     "no substitution when no placeholders",
			input:    "input_path = '/absolute/path/foo.conf'",
			shellDir: "/home/user/shell",
			expected: "input_path = '/absolute/path/foo.conf'",
		},
		{
			name:     "multiple SHELL_DIR occurrences",
			input:    "'SHELL_DIR/a' and 'SHELL_DIR/b'",
			shellDir: "/shell",
			expected: "'/shell/a' and '/shell/b'",
		},
		{
			name:     "only substitutes quoted paths",
			input:    "SHELL_DIR/unquoted and 'SHELL_DIR/quoted'",
			shellDir: "/shell",
			expected: "SHELL_DIR/unquoted and '/shell/quoted'",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := substituteVars(tc.input, tc.shellDir)
			assert.Equal(t, tc.expected, result)
		})
	}
}

func TestBuildMergedConfigColorsOnly(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	baseConfig := "[config]\ncustom_keywords = []\n"
	if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
		t.Fatalf("failed to write base config: %v", err)
	}

	cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
	if err != nil {
		t.Fatalf("failed to create temp config: %v", err)
	}
	defer os.Remove(cfgFile.Name())
	defer cfgFile.Close()

	opts := &Options{
		ShellDir:   shellDir,
		ConfigDir:  filepath.Join(tempDir, "config"),
		StateDir:   filepath.Join(tempDir, "state"),
		ColorsOnly: true,
	}

	if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
		t.Fatalf("buildMergedConfig failed: %v", err)
	}

	if err := cfgFile.Close(); err != nil {
		t.Fatalf("failed to close merged config: %v", err)
	}

	output, err := os.ReadFile(cfgFile.Name())
	if err != nil {
		t.Fatalf("failed to read merged config: %v", err)
	}

	content := string(output)
	assert.Contains(t, content, "[templates.dank]")
	assert.Contains(t, content, "output_path = '"+opts.colorsStaging()+"'")
	assert.NotContains(t, content, "[templates.gtk]")
	assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
}

func TestBuildMergedConfigSkipsMangowcWithoutActiveSession(t *testing.T) {
	t.Setenv("MANGO_INSTANCE_SIGNATURE", "")

	tempDir := t.TempDir()
	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte("[config]\n"), 0o644); err != nil {
		t.Fatalf("failed to write base config: %v", err)
	}
	mangowcConfig := "[templates.dmsmango]\ninput_path = 'in'\noutput_path = 'out'\n"
	if err := os.WriteFile(filepath.Join(configsDir, "mangowc.toml"), []byte(mangowcConfig), 0o644); err != nil {
		t.Fatalf("failed to write mangowc config: %v", err)
	}

	cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
	if err != nil {
		t.Fatalf("failed to create temp config: %v", err)
	}
	defer os.Remove(cfgFile.Name())
	defer cfgFile.Close()

	opts := &Options{
		ShellDir:      shellDir,
		ConfigDir:     filepath.Join(tempDir, "config"),
		StateDir:      filepath.Join(tempDir, "state"),
		SkipTemplates: "gtk,niri,hyprland,qt5ct,qt6ct,firefox,pywalfox,zenbrowser,vesktop,vencord,equibop,ghostty,kitty,foot,alacritty,wezterm,nvim,dgop,kcolorscheme,vscode,emacs,zed",
	}

	if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
		t.Fatalf("buildMergedConfig failed: %v", err)
	}
	if err := cfgFile.Close(); err != nil {
		t.Fatalf("failed to close merged config: %v", err)
	}

	output, err := os.ReadFile(cfgFile.Name())
	if err != nil {
		t.Fatalf("failed to read merged config: %v", err)
	}
	assert.NotContains(t, string(output), "[templates.dmsmango]")
}

func TestAppendConfigConfigDirExists(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	testConfig := "vencord config content"
	if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte(testConfig), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	configHome := filepath.Join(tempDir, "config")
	if err := os.MkdirAll(filepath.Join(configHome, "Vencord"), 0o755); err != nil {
		t.Fatalf("failed to create Vencord config dir: %v", err)
	}
	t.Setenv("XDG_CONFIG_HOME", configHome)

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	mockChecker := mocks_utils.NewMockAppChecker(t)
	mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
	mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)

	opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}

	appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	assert.Equal(t, testConfig+"\n", string(output))
}

func TestAppendConfigConfigDirDoesNotExist(t *testing.T) {
	tempDir := t.TempDir()

	shellDir := filepath.Join(tempDir, "shell")
	configsDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configsDir, 0o755); err != nil {
		t.Fatalf("failed to create configs dir: %v", err)
	}

	if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte("vencord config content"), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	configHome := filepath.Join(tempDir, "config")
	if err := os.MkdirAll(configHome, 0o755); err != nil {
		t.Fatalf("failed to create config home: %v", err)
	}
	t.Setenv("XDG_CONFIG_HOME", configHome)

	outFile := filepath.Join(tempDir, "output.toml")
	cfgFile, err := os.Create(outFile)
	if err != nil {
		t.Fatalf("failed to create output file: %v", err)
	}
	defer cfgFile.Close()

	mockChecker := mocks_utils.NewMockAppChecker(t)
	mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
	mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)

	opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}

	appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")

	cfgFile.Close()
	output, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	assert.Empty(t, string(output))
}

func TestAppendFlatpakConfigRewritesOutputAndTemplateName(t *testing.T) {
	tempDir := t.TempDir()
	shellDir := filepath.Join(tempDir, "shell")
	configDir := filepath.Join(shellDir, "matugen", "configs")
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		t.Fatalf("failed to create config directory: %v", err)
	}

	config := "[templates.dmsvesktop]\ninput_path = 'SHELL_DIR/matugen/templates/vesktop.css'\noutput_path = 'CONFIG_DIR/vesktop/themes/dank-discord.css'\n"
	if err := os.WriteFile(filepath.Join(configDir, "vesktop.toml"), []byte(config), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	outPath := filepath.Join(tempDir, "merged.toml")
	cfgFile, err := os.Create(outPath)
	if err != nil {
		t.Fatalf("failed to create output: %v", err)
	}
	defer cfgFile.Close()

	mockChecker := mocks_utils.NewMockAppChecker(t)
	mockChecker.EXPECT().AnyFlatpakExists("dev.vencord.Vesktop").Return(true)
	t.Setenv("HOME", "/home/test")

	appendFlatpakConfig(&Options{ShellDir: shellDir, AppChecker: mockChecker}, cfgFile, []string{"dev.vencord.Vesktop"}, "vesktop.toml", "dev.vencord.Vesktop/config")
	if err := cfgFile.Close(); err != nil {
		t.Fatalf("failed to close output: %v", err)
	}

	output, err := os.ReadFile(outPath)
	if err != nil {
		t.Fatalf("failed to read output: %v", err)
	}

	assert.Contains(t, string(output), "[templates.dmsvesktop-flatpak]")
	assert.Contains(t, string(output), "'/home/test/.var/app/dev.vencord.Vesktop/config/vesktop/themes/dank-discord.css'")
}
