// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors

package config

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

	"github.com/stretchr/testify/require"
)

// Tests for buildModelWithProtocol helper function.

func TestBuildModelWithProtocol_NoPrefix(t *testing.T) {
	result := buildModelWithProtocol("openai", "gpt-5.4")
	if result != "openai/gpt-5.4" {
		t.Errorf("buildModelWithProtocol(openai, gpt-5.4) = %q, want %q", result, "openai/gpt-5.4")
	}
}

func TestBuildModelWithProtocol_AlreadyHasPrefix(t *testing.T) {
	result := buildModelWithProtocol("openrouter", "openrouter/auto")
	if result != "openrouter/auto" {
		t.Errorf("buildModelWithProtocol(openrouter, openrouter/auto) = %q, want %q", result, "openrouter/auto")
	}
}

func TestBuildModelWithProtocol_DifferentPrefix(t *testing.T) {
	result := buildModelWithProtocol("anthropic", "openrouter/claude-sonnet-4.6")
	if result != "openrouter/claude-sonnet-4.6" {
		t.Errorf(
			"buildModelWithProtocol(anthropic, openrouter/claude-sonnet-4.6) = %q, want %q",
			result,
			"openrouter/claude-sonnet-4.6",
		)
	}
}

// ---------------------------------------------------------------------------
// V0/V1/V2 → V3 migration tests
// ---------------------------------------------------------------------------

// TestLoadConfig_V0MigrateProducesV2 verifies that V0→V3 migration produces
// correct Enabled fields and version.
func TestLoadConfig_V0MigrateProducesV2(t *testing.T) {
	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.json")

	v0Config := `{
		"model_list": [
			{
				"model_name": "gpt-4",
				"model": "openai/gpt-4",
				"api_key": "sk-test"
			},
			{
				"model_name": "claude",
				"model": "anthropic/claude"
			},
			{
				"model_name": "local-model",
				"model": "vllm/custom-model"
			}
		],
		"gateway": {"host": "127.0.0.1", "port": 18790}
	}`

	if err := os.WriteFile(configPath, []byte(v0Config), 0o600); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}

	cfg, err := LoadConfig(configPath)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if cfg.Version != CurrentVersion {
		t.Errorf("Version = %d, want %d", cfg.Version, CurrentVersion)
	}

	// Check enabled status
	modelEnabled := func(name string) bool {
		m, err := cfg.GetModelConfig(name)
		if err != nil {
			return false
		}
		return m.Enabled
	}

	if !modelEnabled("gpt-4") {
		t.Error("gpt-4 with API key from V0 should be enabled")
	}
	if modelEnabled("claude") {
		t.Error("claude without API key from V0 should be disabled")
	}
	if !modelEnabled("local-model") {
		t.Error("local-model from V0 should be enabled")
	}
}

// TestLoadConfig_UnsupportedVersion verifies that unsupported versions return an error.
func TestLoadConfig_UnsupportedVersion(t *testing.T) {
	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.json")

	badConfig := `{"version": 99, "gateway": {"host": "127.0.0.1", "port": 18790}}`
	if err := os.WriteFile(configPath, []byte(badConfig), 0o600); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}

	_, err := LoadConfig(configPath)
	if err == nil {
		t.Fatal("LoadConfig should return error for unsupported version")
	}
	if !containsString(err.Error(), "unsupported config version") {
		t.Errorf("error = %q, want 'unsupported config version'", err.Error())
	}
}

func containsString(s, substr string) bool {
	return len(s) >= len(substr) && searchString(s, substr)
}

func searchString(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}

// TestMigrateV0ToV3 verifies V0 (legacy, no version) → V3 migration.
// V0 configs use the old providers format without model_list.
func TestMigrateV0ToV3(t *testing.T) {
	// V0 config: no version field, uses legacy providers
	v0Config := `{
		"agents": {
			"defaults": {
				"provider": "openai",
				"model": "gpt-4"
			}
		},
		"providers": {
			"openai": {
				"api_key": "sk-test123",
				"api_base": "https://api.openai.com/v1"
			}
		},
		"channels": {
			"telegram": {
				"token": "bot-token"
			},
			"discord": {
				"mention_only": true
			}
		}
	}`

	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.json")
	require.NoError(t, os.WriteFile(configPath, []byte(v0Config), 0o600))
	m, err := loadConfigMap(configPath)
	require.NoError(t, err)

	err = migrateV0ToV1(m)
	require.NoError(t, err)
	err = migrateV1ToV2(m)
	require.NoError(t, err)
	err = migrateV2ToV3(m)
	require.NoError(t, err)

	// Version should be set to CurrentVersion
	require.Equal(t, CurrentVersion, m["version"])

	// Providers should be converted to model_list
	modelList, ok := m["model_list"].([]any)
	require.True(t, ok, "model_list should exist")
	require.NotEmpty(t, modelList, "model_list should not be empty")

	t.Logf("modelList: %+v", modelList)
	// First model should be the user's configured provider with user's model
	firstModel := modelList[0].(map[string]any)
	require.Equal(t, "openai", firstModel["model_name"])
	require.Equal(t, "openai/gpt-4", firstModel["model"])
	// api_key is converted to api_keys during migration
	require.Contains(t, firstModel, "api_keys", "api_keys should exist")

	// Channels should be converted to nested format with channel_list
	channelList, ok := m["channel_list"].(map[string]any)
	require.True(t, ok, "channel_list should exist")
	require.NotContains(t, m, "channels", "old 'channels' key should be removed")

	// telegram channel should have settings
	telegram := channelList["telegram"].(map[string]any)
	require.Equal(t, "telegram", telegram["type"])
	require.Contains(t, telegram, "settings", "telegram should have settings")
	settings := telegram["settings"].(map[string]any)
	require.Equal(t, "bot-token", settings["token"])

	// discord channel should have group_trigger and mention_only in group_trigger
	discord := channelList["discord"].(map[string]any)
	require.Equal(t, "discord", discord["type"])
	discordGroupTrigger := discord["group_trigger"].(map[string]any)
	require.Equal(t, true, discordGroupTrigger["mention_only"])
}

// TestMigrateV0ToV3_WithExistingModelList preserves existing model_list when present.
func TestMigrateV0ToV3_WithExistingModelList(t *testing.T) {
	v0Config := `{
		"model_list": [
			{"model_name": "custom", "model": "openai/custom-model", "api_key": "sk-existing"}
		],
		"channels": {
			"telegram": {"token": "bot123"}
		}
	}`
	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.json")
	require.NoError(t, os.WriteFile(configPath, []byte(v0Config), 0o600))
	m, err := loadConfigMap(configPath)
	require.NoError(t, err)

	err = migrateV0ToV1(m)
	require.NoError(t, err)
	err = migrateV1ToV2(m)
	require.NoError(t, err)
	err = migrateV2ToV3(m)
	require.NoError(t, err)

	// Existing model_list should be preserved (not overridden by providers)
	modelList := m["model_list"].([]any)
	require.Len(t, modelList, 1)
	firstModel := modelList[0].(map[string]any)
	require.Equal(t, "custom", firstModel["model_name"])
}

// TestMigrateV1ToV3 verifies V1 → V3 migration.
// V1 uses flat channel format without "settings" wrapper.
func TestMigrateV1ToV3(t *testing.T) {
	v1Config := `{
		"version": 1,
		"model_list": [
			{"model_name": "gpt-4", "model": "openai/gpt-4", "api_key": "sk-test"}
		],
		"channels": {
			"telegram": {
				"token": "bot-token",
				"base_url": "https://custom.api.com"
			},
			"discord": {
				"mention_only": true,
				"proxy": "socks5://localhost:1080"
			},
			"onebot": {
				"ws_url": "ws://localhost:3001",
				"group_trigger_prefix": ["/"]
			}
		}
	}`

	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.json")
	require.NoError(t, os.WriteFile(configPath, []byte(v1Config), 0o600))
	m, err := loadConfigMap(configPath)
	require.NoError(t, err)

	err = migrateV1ToV2(m)
	require.NoError(t, err)
	err = migrateV2ToV3(m)
	require.NoError(t, err)

	// Version should be set to CurrentVersion
	require.Equal(t, CurrentVersion, m["version"])

	// Channels should be converted to nested format
	channelList, ok := m["channel_list"].(map[string]any)
	require.True(t, ok, "channel_list should exist")
	require.NotContains(t, m, "channels", "old 'channels' key should be removed")

	// telegram: flat fields moved to settings
	telegram := channelList["telegram"].(map[string]any)
	require.Equal(t, "telegram", telegram["type"])
	tgSettings := telegram["settings"].(map[string]any)
	require.Equal(t, "bot-token", tgSettings["token"])
	require.Equal(t, "https://custom.api.com", tgSettings["base_url"])

	// discord: mention_only should be moved to group_trigger
	discord := channelList["discord"].(map[string]any)
	require.Equal(t, "discord", discord["type"])
	require.Contains(t, discord, "group_trigger", "mention_only should be migrated to group_trigger")
	gt := discord["group_trigger"].(map[string]any)
	require.Equal(t, true, gt["mention_only"])
	discordSettings := discord["settings"].(map[string]any)
	require.Equal(t, "socks5://localhost:1080", discordSettings["proxy"])

	// onebot: group_trigger_prefix should be moved to group_trigger.prefixes
	onebot := channelList["onebot"].(map[string]any)
	require.Equal(t, "onebot", onebot["type"])
	obGroupTrigger := onebot["group_trigger"].(map[string]any)
	require.Equal(
		t,
		[]any{"/"},
		obGroupTrigger["prefixes"],
		"group_trigger_prefix should be moved to group_trigger.prefixes",
	)
	obSettings := onebot["settings"].(map[string]any)
	require.Equal(t, "ws://localhost:3001", obSettings["ws_url"])
}

// TestMigrateV1ToV3_ApiKeyConversion verifies api_key → api_keys conversion.
func TestMigrateV1ToV3_ApiKeyConversion(t *testing.T) {
	v1Config := `{
		"version": 1,
		"model_list": [
			{"model_name": "gpt-4", "model": "openai/gpt-4", "api_key": "sk-single"},
			{"model_name": "no-key", "model": "openai/no-key"}
		],
		"channels": {
			"telegram": {"token": "bot"}
		}
	}`

	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.json")
	require.NoError(t, os.WriteFile(configPath, []byte(v1Config), 0o600))
	m, err := loadConfigMap(configPath)
	require.NoError(t, err)

	err = migrateV1ToV2(m)
	require.NoError(t, err)
	err = migrateV2ToV3(m)
	require.NoError(t, err)

	// api_key should be converted to api_keys array
	modelList := m["model_list"].([]any)
	firstModel := modelList[0].(map[string]any)
	require.NotContains(t, firstModel, "api_key", "api_key should be removed")
	require.Contains(t, firstModel, "api_keys", "api_keys should exist")
	// api_keys can be []string or []any depending on how it was set
	if apiKeys, ok := firstModel["api_keys"].([]string); ok {
		require.Len(t, apiKeys, 1)
		require.Equal(t, "sk-single", apiKeys[0])
	} else if apiKeys, ok := firstModel["api_keys"].([]any); ok {
		require.Len(t, apiKeys, 1)
		require.Equal(t, "sk-single", apiKeys[0])
	} else {
		t.Fatalf("api_keys has unexpected type: %T", firstModel["api_keys"])
	}

	// Model without api_key should not have api_keys added
	secondModel := modelList[1].(map[string]any)
	require.NotContains(t, secondModel, "api_key")
	require.NotContains(t, secondModel, "api_keys")
}

// TestMigrateV1ToV3_AlreadyNestedFormat leaves already-nested channels unchanged.
func TestMigrateV1ToV3_AlreadyNestedFormat(t *testing.T) {
	v1Config := `{
		"version": 1,
		"model_list": [
			{"model_name": "gpt-4", "model": "openai/gpt-4"}
		],
		"channels": {
			"telegram": {
				"type": "telegram",
				"settings": {
					"token": "bot-token"
				}
			}
		}
	}`

	tmpDir := t.TempDir()
	configPath := filepath.Join(tmpDir, "config.json")
	require.NoError(t, os.WriteFile(configPath, []byte(v1Config), 0o600))
	m, err := loadConfigMap(configPath)
	require.NoError(t, err)

	err = migrateV1ToV2(m)
	require.NoError(t, err)
	err = migrateV2ToV3(m)
	require.NoError(t, err)

	channelList := m["channel_list"].(map[string]any)
	telegram := channelList["telegram"].(map[string]any)
	// Should not be double-wrapped
	require.Equal(t, "telegram", telegram["type"])
	settings := telegram["settings"].(map[string]any)
	require.Equal(t, "bot-token", settings["token"])
	// Should NOT have nested settings inside settings
	require.NotContains(t, settings, "settings")
}
