package config

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

// setTestHome sets both HOME (Unix) and USERPROFILE (Windows) for cross-platform tests
func setTestHome(t *testing.T, dir string) {
	t.Setenv("HOME", dir)
	t.Setenv("TMPDIR", dir)
	t.Setenv("USERPROFILE", dir)
}

func TestIntegrationConfig(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	t.Run("save and load round-trip", func(t *testing.T) {
		models := []string{"llama3.2", "mistral", "qwen2.5"}
		if err := SaveIntegration("claude", models); err != nil {
			t.Fatal(err)
		}

		config, err := LoadIntegration("claude")
		if err != nil {
			t.Fatal(err)
		}

		if len(config.Models) != len(models) {
			t.Errorf("expected %d models, got %d", len(models), len(config.Models))
		}
		for i, m := range models {
			if config.Models[i] != m {
				t.Errorf("model %d: expected %s, got %s", i, m, config.Models[i])
			}
		}
	})

	t.Run("save and load aliases", func(t *testing.T) {
		models := []string{"llama3.2"}
		if err := SaveIntegration("claude", models); err != nil {
			t.Fatal(err)
		}
		aliases := map[string]string{
			"primary": "llama3.2:70b",
			"fast":    "llama3.2:8b",
		}
		if err := SaveAliases("claude", aliases); err != nil {
			t.Fatal(err)
		}

		config, err := LoadIntegration("claude")
		if err != nil {
			t.Fatal(err)
		}
		if config.Aliases == nil {
			t.Fatal("expected aliases to be saved")
		}
		for k, v := range aliases {
			if config.Aliases[k] != v {
				t.Errorf("alias %s: expected %s, got %s", k, v, config.Aliases[k])
			}
		}
	})

	t.Run("saveIntegration preserves aliases", func(t *testing.T) {
		if err := SaveIntegration("claude", []string{"model-a"}); err != nil {
			t.Fatal(err)
		}
		if err := SaveAliases("claude", map[string]string{"primary": "model-a", "fast": "model-small"}); err != nil {
			t.Fatal(err)
		}

		if err := SaveIntegration("claude", []string{"model-b"}); err != nil {
			t.Fatal(err)
		}
		config, err := LoadIntegration("claude")
		if err != nil {
			t.Fatal(err)
		}
		if config.Aliases["primary"] != "model-a" {
			t.Errorf("expected aliases to be preserved, got %v", config.Aliases)
		}
	})

	t.Run("defaultModel returns first model", func(t *testing.T) {
		SaveIntegration("codex", []string{"model-a", "model-b"})

		config, _ := LoadIntegration("codex")
		defaultModel := ""
		if len(config.Models) > 0 {
			defaultModel = config.Models[0]
		}
		if defaultModel != "model-a" {
			t.Errorf("expected model-a, got %s", defaultModel)
		}
	})

	t.Run("defaultModel returns empty for no models", func(t *testing.T) {
		config := &integration{Models: []string{}}
		defaultModel := ""
		if len(config.Models) > 0 {
			defaultModel = config.Models[0]
		}
		if defaultModel != "" {
			t.Errorf("expected empty string, got %s", defaultModel)
		}
	})

	t.Run("app name is case-insensitive", func(t *testing.T) {
		SaveIntegration("Claude", []string{"model-x"})

		config, err := LoadIntegration("claude")
		if err != nil {
			t.Fatal(err)
		}
		defaultModel := ""
		if len(config.Models) > 0 {
			defaultModel = config.Models[0]
		}
		if defaultModel != "model-x" {
			t.Errorf("expected model-x, got %s", defaultModel)
		}
	})

	t.Run("multiple integrations in single file", func(t *testing.T) {
		SaveIntegration("app1", []string{"model-1"})
		SaveIntegration("app2", []string{"model-2"})

		config1, _ := LoadIntegration("app1")
		config2, _ := LoadIntegration("app2")

		defaultModel1 := ""
		if len(config1.Models) > 0 {
			defaultModel1 = config1.Models[0]
		}
		defaultModel2 := ""
		if len(config2.Models) > 0 {
			defaultModel2 = config2.Models[0]
		}
		if defaultModel1 != "model-1" {
			t.Errorf("expected model-1, got %s", defaultModel1)
		}
		if defaultModel2 != "model-2" {
			t.Errorf("expected model-2, got %s", defaultModel2)
		}
	})
}

func TestListIntegrations(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	t.Run("returns empty when no integrations", func(t *testing.T) {
		configs, err := listIntegrations()
		if err != nil {
			t.Fatal(err)
		}
		if len(configs) != 0 {
			t.Errorf("expected 0 integrations, got %d", len(configs))
		}
	})

	t.Run("returns all saved integrations", func(t *testing.T) {
		SaveIntegration("claude", []string{"model-1"})
		SaveIntegration("droid", []string{"model-2"})

		configs, err := listIntegrations()
		if err != nil {
			t.Fatal(err)
		}
		if len(configs) != 2 {
			t.Errorf("expected 2 integrations, got %d", len(configs))
		}
	})
}

func TestLoadIntegration_CorruptedJSON(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	dir := filepath.Join(tmpDir, ".ollama")
	os.MkdirAll(dir, 0o755)
	os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{corrupted json`), 0o644)

	_, err := LoadIntegration("test")
	if err == nil {
		t.Error("expected error for nonexistent integration in corrupted file")
	}
}

func TestSaveIntegration_NilModels(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	if err := SaveIntegration("test", nil); err != nil {
		t.Fatalf("saveIntegration with nil models failed: %v", err)
	}

	config, err := LoadIntegration("test")
	if err != nil {
		t.Fatalf("loadIntegration failed: %v", err)
	}

	if config.Models == nil {
		// nil is acceptable
	} else if len(config.Models) != 0 {
		t.Errorf("expected empty or nil models, got %v", config.Models)
	}
}

func TestSaveIntegration_EmptyAppName(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	err := SaveIntegration("", []string{"model"})
	if err == nil {
		t.Error("expected error for empty app name, got nil")
	}
	if err != nil && !strings.Contains(err.Error(), "app name cannot be empty") {
		t.Errorf("expected 'app name cannot be empty' error, got: %v", err)
	}
}

func TestLoadIntegration_NonexistentIntegration(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	_, err := LoadIntegration("nonexistent")
	if err == nil {
		t.Error("expected error for nonexistent integration, got nil")
	}
	if !os.IsNotExist(err) {
		t.Logf("error type is os.ErrNotExist as expected: %v", err)
	}
}

func TestConfigPath(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	path, err := configPath()
	if err != nil {
		t.Fatal(err)
	}

	expected := filepath.Join(tmpDir, ".ollama", "config.json")
	if path != expected {
		t.Errorf("expected %s, got %s", expected, path)
	}
}

func TestLoad(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	t.Run("returns empty config when file does not exist", func(t *testing.T) {
		cfg, err := load()
		if err != nil {
			t.Fatal(err)
		}
		if cfg == nil {
			t.Fatal("expected non-nil config")
		}
		if cfg.Integrations == nil {
			t.Error("expected non-nil Integrations map")
		}
		if len(cfg.Integrations) != 0 {
			t.Errorf("expected empty Integrations, got %d", len(cfg.Integrations))
		}
	})

	t.Run("loads existing config", func(t *testing.T) {
		path, _ := configPath()
		os.MkdirAll(filepath.Dir(path), 0o755)
		os.WriteFile(path, []byte(`{"integrations":{"test":{"models":["model-a"]}}}`), 0o644)

		cfg, err := load()
		if err != nil {
			t.Fatal(err)
		}
		if cfg.Integrations["test"] == nil {
			t.Fatal("expected test integration")
		}
		if len(cfg.Integrations["test"].Models) != 1 {
			t.Errorf("expected 1 model, got %d", len(cfg.Integrations["test"].Models))
		}
	})

	t.Run("returns error for corrupted JSON", func(t *testing.T) {
		path, _ := configPath()
		os.MkdirAll(filepath.Dir(path), 0o755)
		os.WriteFile(path, []byte(`{corrupted`), 0o644)

		_, err := load()
		if err == nil {
			t.Error("expected error for corrupted JSON")
		}
	})
}

func TestMigrateConfig(t *testing.T) {
	t.Run("migrates legacy file to new location", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		legacyDir := filepath.Join(tmpDir, ".ollama", "config")
		os.MkdirAll(legacyDir, 0o755)
		data := []byte(`{"integrations":{"claude":{"models":["llama3.2"]}}}`)
		os.WriteFile(filepath.Join(legacyDir, "config.json"), data, 0o644)

		migrated, err := migrateConfig()
		if err != nil {
			t.Fatal(err)
		}
		if !migrated {
			t.Fatal("expected migration to occur")
		}

		newPath, _ := configPath()
		got, err := os.ReadFile(newPath)
		if err != nil {
			t.Fatalf("new config not found: %v", err)
		}
		if string(got) != string(data) {
			t.Errorf("content mismatch: got %s", got)
		}

		if _, err := os.Stat(filepath.Join(legacyDir, "config.json")); !os.IsNotExist(err) {
			t.Error("legacy file should have been removed")
		}

		if _, err := os.Stat(legacyDir); !os.IsNotExist(err) {
			t.Error("legacy directory should have been removed")
		}
	})

	t.Run("no-op when no legacy file exists", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		migrated, err := migrateConfig()
		if err != nil {
			t.Fatal(err)
		}
		if migrated {
			t.Error("expected no migration")
		}
	})

	t.Run("skips corrupt legacy file", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		legacyDir := filepath.Join(tmpDir, ".ollama", "config")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "config.json"), []byte(`{corrupt`), 0o644)

		migrated, err := migrateConfig()
		if err != nil {
			t.Fatal(err)
		}
		if migrated {
			t.Error("should not migrate corrupt file")
		}

		if _, err := os.Stat(filepath.Join(legacyDir, "config.json")); os.IsNotExist(err) {
			t.Error("corrupt legacy file should not have been deleted")
		}
	})

	t.Run("new path takes precedence over legacy", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		legacyDir := filepath.Join(tmpDir, ".ollama", "config")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "config.json"), []byte(`{"integrations":{"old":{"models":["old-model"]}}}`), 0o644)

		newDir := filepath.Join(tmpDir, ".ollama")
		os.WriteFile(filepath.Join(newDir, "config.json"), []byte(`{"integrations":{"new":{"models":["new-model"]}}}`), 0o644)

		cfg, err := load()
		if err != nil {
			t.Fatal(err)
		}
		if _, ok := cfg.Integrations["new"]; !ok {
			t.Error("expected new-path integration to be loaded")
		}
		if _, ok := cfg.Integrations["old"]; ok {
			t.Error("legacy integration should not have been loaded")
		}
	})

	t.Run("idempotent when called twice", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		legacyDir := filepath.Join(tmpDir, ".ollama", "config")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "config.json"), []byte(`{"integrations":{}}`), 0o644)

		if _, err := migrateConfig(); err != nil {
			t.Fatal(err)
		}

		migrated, err := migrateConfig()
		if err != nil {
			t.Fatal(err)
		}
		if migrated {
			t.Error("second migration should be a no-op")
		}
	})

	t.Run("legacy directory preserved if not empty", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		legacyDir := filepath.Join(tmpDir, ".ollama", "config")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "config.json"), []byte(`{"integrations":{}}`), 0o644)
		os.WriteFile(filepath.Join(legacyDir, "other-file.txt"), []byte("keep me"), 0o644)

		if _, err := migrateConfig(); err != nil {
			t.Fatal(err)
		}

		if _, err := os.Stat(legacyDir); os.IsNotExist(err) {
			t.Error("directory with other files should not have been removed")
		}
		if _, err := os.Stat(filepath.Join(legacyDir, "other-file.txt")); os.IsNotExist(err) {
			t.Error("other files in legacy directory should be untouched")
		}
	})

	t.Run("save writes to new path after migration", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		legacyDir := filepath.Join(tmpDir, ".ollama", "config")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "config.json"), []byte(`{"integrations":{"claude":{"models":["llama3.2"]}}}`), 0o644)

		// load triggers migration, then save should write to new path
		if err := SaveIntegration("codex", []string{"qwen2.5"}); err != nil {
			t.Fatal(err)
		}

		newPath := filepath.Join(tmpDir, ".ollama", "config.json")
		if _, err := os.Stat(newPath); os.IsNotExist(err) {
			t.Error("save should write to new path")
		}

		// old path should not be recreated
		if _, err := os.Stat(filepath.Join(legacyDir, "config.json")); !os.IsNotExist(err) {
			t.Error("save should not recreate legacy path")
		}
	})

	t.Run("load triggers migration transparently", func(t *testing.T) {
		tmpDir := t.TempDir()
		setTestHome(t, tmpDir)

		legacyDir := filepath.Join(tmpDir, ".ollama", "config")
		os.MkdirAll(legacyDir, 0o755)
		os.WriteFile(filepath.Join(legacyDir, "config.json"), []byte(`{"integrations":{"claude":{"models":["llama3.2"]}}}`), 0o644)

		cfg, err := load()
		if err != nil {
			t.Fatal(err)
		}
		if cfg.Integrations["claude"] == nil || cfg.Integrations["claude"].Models[0] != "llama3.2" {
			t.Error("migration via load() did not preserve data")
		}
	})
}

func TestSave(t *testing.T) {
	tmpDir := t.TempDir()
	setTestHome(t, tmpDir)

	t.Run("creates config file", func(t *testing.T) {
		cfg := &config{
			Integrations: map[string]*integration{
				"test": {Models: []string{"model-a", "model-b"}},
			},
		}

		if err := save(cfg); err != nil {
			t.Fatal(err)
		}

		path, _ := configPath()
		if _, err := os.Stat(path); os.IsNotExist(err) {
			t.Error("config file was not created")
		}
	})

	t.Run("round-trip preserves data", func(t *testing.T) {
		cfg := &config{
			Integrations: map[string]*integration{
				"claude": {Models: []string{"llama3.2", "mistral"}},
				"codex":  {Models: []string{"qwen2.5"}},
			},
		}

		if err := save(cfg); err != nil {
			t.Fatal(err)
		}

		loaded, err := load()
		if err != nil {
			t.Fatal(err)
		}

		if len(loaded.Integrations) != 2 {
			t.Errorf("expected 2 integrations, got %d", len(loaded.Integrations))
		}
		if loaded.Integrations["claude"] == nil {
			t.Error("missing claude integration")
		}
		if len(loaded.Integrations["claude"].Models) != 2 {
			t.Errorf("expected 2 models for claude, got %d", len(loaded.Integrations["claude"].Models))
		}
	})
}
