package plugins

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

	"github.com/AvengeMedia/DankMaterialShell/core/internal/registries"
	"github.com/go-git/go-git/v6"
	"github.com/go-git/go-git/v6/plumbing/object"
	"github.com/spf13/afero"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const testRegistryURL = "https://example.com/test-registry.git"

type mockGitClient struct {
	cloneFunc      func(path string, url string) error
	pullFunc       func(path string) error
	originFunc     func(path string) (string, error)
	hasUpdatesFunc func(path string) (bool, string, string, error)
	revisionFunc   func(path string) (string, error)
	checkoutFunc   func(path string, revision string) error
}

func (m *mockGitClient) PlainClone(path string, url string) error {
	if m.cloneFunc != nil {
		return m.cloneFunc(path, url)
	}
	return nil
}

func (m *mockGitClient) Pull(path string) error {
	if m.pullFunc != nil {
		return m.pullFunc(path)
	}
	return nil
}

func (m *mockGitClient) OriginURL(path string) (string, error) {
	if m.originFunc != nil {
		return m.originFunc(path)
	}
	return "https://github.com/test/plugin.git", nil
}

func (m *mockGitClient) HasUpdates(path string) (bool, string, string, error) {
	if m.hasUpdatesFunc != nil {
		return m.hasUpdatesFunc(path)
	}
	return false, "", "", nil
}

func (m *mockGitClient) CurrentRevision(path string) (string, error) {
	if m.revisionFunc != nil {
		return m.revisionFunc(path)
	}
	return "0123456789abcdef0123456789abcdef01234567", nil
}

func (m *mockGitClient) CheckoutRevision(path string, revision string) error {
	if m.checkoutFunc != nil {
		return m.checkoutFunc(path, revision)
	}
	return nil
}

func TestNewRegistry(t *testing.T) {
	registry, err := NewRegistry()
	assert.NoError(t, err)
	assert.NotNil(t, registry)
	assert.NotEmpty(t, registry.cacheDir)
	require.NotEmpty(t, registry.registries)
	assert.Equal(t, registries.OfficialName, registry.registries[0].Name)
}

func TestGetCacheDir(t *testing.T) {
	cacheDir := getCacheDir()
	assert.Contains(t, cacheDir, "/tmp/dankdots-plugin-registry")
}

func TestRealGitClientRevisionCheckout(t *testing.T) {
	repoPath := t.TempDir()
	repo, err := git.PlainInit(repoPath, false)
	require.NoError(t, err)
	worktree, err := repo.Worktree()
	require.NoError(t, err)
	filePath := filepath.Join(repoPath, "value.txt")
	signature := &object.Signature{Name: "DMS Test", Email: "test@example.com", When: time.Unix(1, 0)}

	require.NoError(t, os.WriteFile(filePath, []byte("one"), 0o644))
	_, err = worktree.Add("value.txt")
	require.NoError(t, err)
	first, err := worktree.Commit("first", &git.CommitOptions{Author: signature})
	require.NoError(t, err)

	require.NoError(t, os.WriteFile(filePath, []byte("two"), 0o644))
	_, err = worktree.Add("value.txt")
	require.NoError(t, err)
	second, err := worktree.Commit("second", &git.CommitOptions{Author: signature})
	require.NoError(t, err)

	client := &realGitClient{}
	revision, err := client.CurrentRevision(repoPath)
	require.NoError(t, err)
	assert.Equal(t, second.String(), revision)
	require.NoError(t, client.CheckoutRevision(repoPath, first.String()))

	revision, err = client.CurrentRevision(repoPath)
	require.NoError(t, err)
	assert.Equal(t, first.String(), revision)
	data, err := os.ReadFile(filePath)
	require.NoError(t, err)
	assert.Equal(t, "one", string(data))
}

func setupTestRegistry(t *testing.T) (*Registry, afero.Fs, string) {
	fs := afero.NewMemMapFs()
	tmpDir := "/test-cache"
	registry := &Registry{
		fs:         fs,
		cacheDir:   tmpDir,
		registries: []registries.Source{{Name: "test", URL: testRegistryURL}},
		plugins:    []Plugin{},
		git:        &mockGitClient{},
	}
	return registry, fs, tmpDir
}

func createTestPlugin(t *testing.T, fs afero.Fs, dir string, filename string, plugin Plugin) {
	pluginsDir := filepath.Join(dir, "plugins")
	err := fs.MkdirAll(pluginsDir, 0o755)
	require.NoError(t, err)

	data, err := json.Marshal(plugin)
	require.NoError(t, err)

	err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0o644)
	require.NoError(t, err)
}

func TestLoadPlugins(t *testing.T) {
	t.Run("loads valid plugin files", func(t *testing.T) {
		registry, fs, tmpDir := setupTestRegistry(t)

		plugin1 := Plugin{
			Name:         "TestPlugin1",
			Capabilities: []string{"dankbar-widget"},
			Category:     "monitoring",
			Repo:         "https://github.com/test/plugin1",
			Author:       "Test Author",
			Description:  "Test plugin 1",
			Compositors:  []string{"niri"},
			Distro:       []string{"any"},
		}

		plugin2 := Plugin{
			Name:         "TestPlugin2",
			Capabilities: []string{"system-tray"},
			Category:     "utilities",
			Repo:         "https://github.com/test/plugin2",
			Author:       "Another Author",
			Description:  "Test plugin 2",
			Dependencies: []string{"dep1", "dep2"},
			Compositors:  []string{"hyprland", "niri"},
			Distro:       []string{"arch"},
			Screenshot:   "https://example.com/screenshot.png",
		}

		createTestPlugin(t, fs, tmpDir, "plugin1.json", plugin1)
		createTestPlugin(t, fs, tmpDir, "plugin2.json", plugin2)

		plugins, err := registry.loadPluginsFrom(tmpDir)
		assert.NoError(t, err)
		assert.Len(t, plugins, 2)

		assert.Equal(t, "TestPlugin1", plugins[0].Name)
		assert.Equal(t, "TestPlugin2", plugins[1].Name)
		assert.Equal(t, []string{"dankbar-widget"}, plugins[0].Capabilities)
		assert.Equal(t, []string{"dep1", "dep2"}, plugins[1].Dependencies)
	})

	t.Run("skips non-json files", func(t *testing.T) {
		registry, fs, tmpDir := setupTestRegistry(t)

		pluginsDir := filepath.Join(tmpDir, "plugins")
		err := fs.MkdirAll(pluginsDir, 0o755)
		require.NoError(t, err)

		err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0o644)
		require.NoError(t, err)

		plugin := Plugin{
			Name:         "ValidPlugin",
			Capabilities: []string{"test"},
			Category:     "test",
			Repo:         "https://github.com/test/test",
			Author:       "Test",
			Description:  "Test",
			Compositors:  []string{"niri"},
			Distro:       []string{"any"},
		}
		createTestPlugin(t, fs, tmpDir, "valid.json", plugin)

		plugins, err := registry.loadPluginsFrom(tmpDir)
		assert.NoError(t, err)
		assert.Len(t, plugins, 1)
		assert.Equal(t, "ValidPlugin", plugins[0].Name)
	})

	t.Run("skips invalid json files", func(t *testing.T) {
		registry, fs, tmpDir := setupTestRegistry(t)

		pluginsDir := filepath.Join(tmpDir, "plugins")
		err := fs.MkdirAll(pluginsDir, 0o755)
		require.NoError(t, err)

		err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0o644)
		require.NoError(t, err)

		plugin := Plugin{
			Name:         "ValidPlugin",
			Capabilities: []string{"test"},
			Category:     "test",
			Repo:         "https://github.com/test/test",
			Author:       "Test",
			Description:  "Test",
			Compositors:  []string{"niri"},
			Distro:       []string{"any"},
		}
		createTestPlugin(t, fs, tmpDir, "valid.json", plugin)

		plugins, err := registry.loadPluginsFrom(tmpDir)
		assert.NoError(t, err)
		assert.Len(t, plugins, 1)
		assert.Equal(t, "ValidPlugin", plugins[0].Name)
	})

	t.Run("missing plugins directory is a themes-only registry", func(t *testing.T) {
		registry, _, _ := setupTestRegistry(t)

		plugins, err := registry.loadPluginsFrom(registry.cacheDir)
		assert.NoError(t, err)
		assert.Empty(t, plugins)
	})
}

func TestList(t *testing.T) {
	t.Run("returns cached plugins if available", func(t *testing.T) {
		registry, _, _ := setupTestRegistry(t)

		plugin := Plugin{
			Name:         "CachedPlugin",
			Capabilities: []string{"test"},
			Category:     "test",
			Repo:         "https://github.com/test/test",
			Author:       "Test",
			Description:  "Test",
			Compositors:  []string{"niri"},
			Distro:       []string{"any"},
		}

		registry.plugins = []Plugin{plugin}

		plugins, err := registry.List()
		assert.NoError(t, err)
		assert.Len(t, plugins, 1)
		assert.Equal(t, "CachedPlugin", plugins[0].Name)
	})

	t.Run("updates and loads plugins when cache is empty", func(t *testing.T) {
		registry, fs, _ := setupTestRegistry(t)

		plugin := Plugin{
			Name:         "NewPlugin",
			Capabilities: []string{"test"},
			Category:     "test",
			Repo:         "https://github.com/test/test",
			Author:       "Test",
			Description:  "Test",
			Compositors:  []string{"niri"},
			Distro:       []string{"any"},
		}

		registry.git = &mockGitClient{
			cloneFunc: func(path string, url string) error {
				createTestPlugin(t, fs, path, "plugin.json", plugin)
				return nil
			},
		}

		plugins, err := registry.List()
		assert.NoError(t, err)
		assert.Len(t, plugins, 1)
		assert.Equal(t, "NewPlugin", plugins[0].Name)
	})

	t.Run("partial registry failure still returns loaded plugins", func(t *testing.T) {
		registry, fs, _ := setupTestRegistry(t)

		registry.registries = []registries.Source{
			{Name: "test", URL: testRegistryURL},
			{Name: "broken", URL: "https://example.com/broken.git"},
		}
		registry.git = &mockGitClient{
			cloneFunc: func(path string, url string) error {
				if url != testRegistryURL {
					return errors.New("clone failed")
				}
				createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
				return nil
			},
		}

		plugins, err := registry.List()
		assert.NoError(t, err)
		assert.Len(t, plugins, 1)
	})
}

func TestUpdate(t *testing.T) {
	t.Run("clones repository when cache doesn't exist", func(t *testing.T) {
		registry, fs, tmpDir := setupTestRegistry(t)

		plugin := Plugin{
			Name:         "RepoPlugin",
			Capabilities: []string{"test"},
			Category:     "test",
			Repo:         "https://github.com/test/test",
			Author:       "Test",
			Description:  "Test",
			Compositors:  []string{"niri"},
			Distro:       []string{"any"},
		}

		cloneCalled := false
		registry.git = &mockGitClient{
			cloneFunc: func(path string, url string) error {
				cloneCalled = true
				assert.Equal(t, testRegistryURL, url)
				assert.Equal(t, filepath.Join(tmpDir, "test"), path)
				createTestPlugin(t, fs, path, "plugin.json", plugin)
				return nil
			},
		}

		err := registry.Update()
		assert.NoError(t, err)
		assert.True(t, cloneCalled)
		assert.Len(t, registry.plugins, 1)
		assert.Equal(t, "RepoPlugin", registry.plugins[0].Name)
	})

	t.Run("pulls when cache exists with matching origin", func(t *testing.T) {
		registry, fs, tmpDir := setupTestRegistry(t)

		plugin := Plugin{
			Name:         "UpdatedPlugin",
			Capabilities: []string{"test"},
			Category:     "test",
			Repo:         "https://github.com/test/test",
			Author:       "Test",
			Description:  "Test",
			Compositors:  []string{"niri"},
			Distro:       []string{"any"},
		}

		subdir := filepath.Join(tmpDir, "test")
		require.NoError(t, fs.MkdirAll(subdir, 0o755))

		pullCalled := false
		registry.git = &mockGitClient{
			originFunc: func(path string) (string, error) {
				return testRegistryURL, nil
			},
			pullFunc: func(path string) error {
				pullCalled = true
				assert.Equal(t, subdir, path)
				createTestPlugin(t, fs, path, "plugin.json", plugin)
				return nil
			},
		}

		err := registry.Update()
		assert.NoError(t, err)
		assert.True(t, pullCalled)
		assert.Len(t, registry.plugins, 1)
		assert.Equal(t, "UpdatedPlugin", registry.plugins[0].Name)
	})

	t.Run("re-clones when cached origin does not match configured URL", func(t *testing.T) {
		registry, fs, tmpDir := setupTestRegistry(t)

		subdir := filepath.Join(tmpDir, "test")
		require.NoError(t, fs.MkdirAll(subdir, 0o755))
		require.NoError(t, afero.WriteFile(fs, filepath.Join(subdir, "stale"), []byte("x"), 0o644))

		pullCalled := false
		cloneCalled := false
		registry.git = &mockGitClient{
			originFunc: func(path string) (string, error) {
				return "https://example.com/old-origin.git", nil
			},
			pullFunc: func(path string) error {
				pullCalled = true
				return nil
			},
			cloneFunc: func(path string, url string) error {
				cloneCalled = true
				assert.Equal(t, testRegistryURL, url)
				createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
				return nil
			},
		}

		err := registry.Update()
		assert.NoError(t, err)
		assert.False(t, pullCalled, "stale origin must not be pulled")
		assert.True(t, cloneCalled)
		exists, _ := afero.Exists(fs, filepath.Join(subdir, "stale"))
		assert.False(t, exists, "stale cache contents removed before re-clone")
	})

	t.Run("re-clones when pull fails", func(t *testing.T) {
		registry, fs, tmpDir := setupTestRegistry(t)

		subdir := filepath.Join(tmpDir, "test")
		require.NoError(t, fs.MkdirAll(subdir, 0o755))

		cloneCalled := false
		registry.git = &mockGitClient{
			originFunc: func(path string) (string, error) {
				return testRegistryURL, nil
			},
			pullFunc: func(path string) error {
				return errors.New("shallow clone corruption")
			},
			cloneFunc: func(path string, url string) error {
				cloneCalled = true
				createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
				return nil
			},
		}

		err := registry.Update()
		assert.NoError(t, err)
		assert.True(t, cloneCalled)
	})

	t.Run("aggregates from multiple registries", func(t *testing.T) {
		registry, fs, _ := setupTestRegistry(t)

		pluginA := Plugin{ID: "a", Name: "PluginA", Compositors: []string{"niri"}, Distro: []string{"any"}}
		pluginB := Plugin{ID: "b", Name: "PluginB", Compositors: []string{"niri"}, Distro: []string{"any"}}

		registry.registries = []registries.Source{
			{Name: "official", URL: testRegistryURL},
			{Name: "louzt", URL: "https://example.com/louzt.git"},
		}

		registry.git = &mockGitClient{
			cloneFunc: func(path string, url string) error {
				switch filepath.Base(path) {
				case "official":
					createTestPlugin(t, fs, path, "x.json", pluginA)
				case "louzt":
					createTestPlugin(t, fs, path, "x.json", pluginB)
				}
				return nil
			},
		}

		err := registry.Update()
		assert.NoError(t, err)
		assert.Len(t, registry.plugins, 2)
		assert.Equal(t, "a", registry.plugins[0].ID)
		assert.Equal(t, "b", registry.plugins[1].ID)
	})

	t.Run("dedupes by ID with declaration order priority", func(t *testing.T) {
		registry, fs, _ := setupTestRegistry(t)

		pluginOfficial := Plugin{ID: "weather", Name: "OfficialWeather", Compositors: []string{"niri"}, Distro: []string{"any"}}
		pluginLouzt := Plugin{ID: "weather", Name: "LouztWeather", Compositors: []string{"niri"}, Distro: []string{"any"}}

		registry.registries = []registries.Source{
			{Name: "official", URL: testRegistryURL},
			{Name: "louzt", URL: "https://example.com/louzt.git"},
		}

		registry.git = &mockGitClient{
			cloneFunc: func(path string, url string) error {
				switch filepath.Base(path) {
				case "official":
					createTestPlugin(t, fs, path, "x.json", pluginOfficial)
				case "louzt":
					createTestPlugin(t, fs, path, "x.json", pluginLouzt)
				}
				return nil
			},
		}

		err := registry.Update()
		assert.NoError(t, err)
		assert.Len(t, registry.plugins, 1)
		assert.Equal(t, "OfficialWeather", registry.plugins[0].Name)
	})

	t.Run("continues past failing registry and reports it", func(t *testing.T) {
		registry, fs, _ := setupTestRegistry(t)

		registry.registries = []registries.Source{
			{Name: "broken", URL: "https://example.com/broken.git"},
			{Name: "test", URL: testRegistryURL},
		}
		registry.git = &mockGitClient{
			cloneFunc: func(path string, url string) error {
				if url != testRegistryURL {
					return errors.New("network unreachable")
				}
				createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
				return nil
			},
		}

		err := registry.Update()
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "registry broken")
		assert.Len(t, registry.plugins, 1, "healthy registry still loads")
	})

	t.Run("removes legacy single-clone cache at base", func(t *testing.T) {
		registry, fs, tmpDir := setupTestRegistry(t)

		require.NoError(t, fs.MkdirAll(filepath.Join(tmpDir, ".git"), 0o755))
		createTestPlugin(t, fs, tmpDir, "legacy.json", Plugin{ID: "legacy", Name: "Legacy"})

		registry.git = &mockGitClient{
			cloneFunc: func(path string, url string) error {
				createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
				return nil
			},
		}

		err := registry.Update()
		assert.NoError(t, err)
		exists, _ := afero.DirExists(fs, filepath.Join(tmpDir, ".git"))
		assert.False(t, exists, "legacy clone removed")
		assert.Len(t, registry.plugins, 1)
		assert.Equal(t, "x", registry.plugins[0].ID)
	})
}
