package config

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

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

// writeYAML writes content to a file named name inside dir. Returns the full
// path of the written file.
func writeYAML(t *testing.T, dir, name, content string) string {
	t.Helper()
	p := filepath.Join(dir, name)
	require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755))
	require.NoError(t, os.WriteFile(p, []byte(content), 0o644))
	return p
}

// modelCfg builds a single-model YAML snippet indented for nesting under a
// `models:` key. The proxy uses a fixed port so tests don't depend on
// ${PORT} allocation.
func modelCfg(id, cmd string) string {
	return "  " + id + ":\n    cmd: " + cmd + "\n    proxy: \"http://localhost:9999\"\n"
}

func TestLoadConfigSources_NeitherProvided(t *testing.T) {
	_, err := LoadConfigSources("", "")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "at least one of -config or -config-dir")
}

func TestLoadConfigSources_ConfigOnly(t *testing.T) {
	dir := t.TempDir()
	cfgPath := writeYAML(t, dir, "config.yaml", `
models:
`+modelCfg("model1", "echo hi")+`
groups:
  group1:
    members: ["model1"]
`)
	cfg, err := LoadConfigSources(cfgPath, "")
	require.NoError(t, err)
	_, id, ok := cfg.FindConfig("model1")
	require.True(t, ok)
	assert.Equal(t, "model1", id)
}

func TestLoadConfigSources_DirOnly(t *testing.T) {
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", "models:\n"+modelCfg("alpha", "echo a"))
	writeYAML(t, dir, "b.yaml", "models:\n"+modelCfg("beta", "echo b"))

	cfg, err := LoadConfigSources("", dir)
	require.NoError(t, err)
	for _, want := range []string{"alpha", "beta"} {
		_, _, ok := cfg.FindConfig(want)
		assert.True(t, ok, "model %s should be present", want)
	}
}

func TestLoadConfigSources_ConfigPlusDirAdditive(t *testing.T) {
	// -config lives outside -config-dir; both contribute models additively.
	dir := t.TempDir()
	cfgPath := writeYAML(t, dir, "config.yaml", "models:\n"+modelCfg("base", "echo base"))
	cfgDir := t.TempDir()
	writeYAML(t, cfgDir, "extra.yaml", "models:\n"+modelCfg("ext", "echo ext"))

	cfg, err := LoadConfigSources(cfgPath, cfgDir)
	require.NoError(t, err)
	for _, want := range []string{"base", "ext"} {
		_, _, ok := cfg.FindConfig(want)
		assert.True(t, ok, "model %s should be present after merge", want)
	}
}

// TestLoadConfigSources_ConfigInDirOverlap verifies that a -config file that
// is also a member of -config-dir is rejected.
func TestLoadConfigSources_ConfigInDirOverlap(t *testing.T) {
	dir := t.TempDir()
	cfgPath := writeYAML(t, dir, "main.yaml", "models:\n"+modelCfg("base", "echo base"))

	_, err := LoadConfigSources(cfgPath, dir)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "is also present in -config-dir")
}

func TestLoadConfigSources_DuplicateModelID(t *testing.T) {
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", "models:\n"+modelCfg("dup", "echo a"))
	writeYAML(t, dir, "b.yaml", "models:\n"+modelCfg("dup", "echo b"))

	_, err := LoadConfigSources("", dir)
	require.Error(t, err)
	assert.Contains(t, err.Error(), `duplicate models "dup"`)
}

func TestLoadConfigSources_DuplicateGroupID(t *testing.T) {
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", `
models:
`+modelCfg("m1", "echo m1")+"groups:\n  g1:\n    members: [m1]\n")
	writeYAML(t, dir, "b.yaml", `
models:
`+modelCfg("m2", "echo m2")+"groups:\n  g1:\n    members: [m2]\n")

	_, err := LoadConfigSources("", dir)
	require.Error(t, err)
	assert.Contains(t, err.Error(), `duplicate groups "g1"`)
}

func TestLoadConfigSources_DuplicatePeer(t *testing.T) {
	dir := t.TempDir()
	peerA := "peers:\n  remote:\n    proxy: http://x:1\n    models: [m1]\n"
	peerB := "peers:\n  remote:\n    proxy: http://x:2\n    models: [m2]\n"
	writeYAML(t, dir, "a.yaml", "models:\n"+modelCfg("m1", "echo m1")+"\n"+peerA)
	writeYAML(t, dir, "b.yaml", "models:\n"+modelCfg("m2", "echo m2")+"\n"+peerB)

	_, err := LoadConfigSources("", dir)
	require.Error(t, err)
	assert.Contains(t, err.Error(), `duplicate peers "remote"`)
}

func TestLoadConfigSources_ScalarConflict(t *testing.T) {
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", "models:\n"+modelCfg("m1", "echo m1")+"\nglobalTTL: 100\n")
	writeYAML(t, dir, "b.yaml", "models:\n"+modelCfg("m2", "echo m2")+"\nglobalTTL: 200\n")

	_, err := LoadConfigSources("", dir)
	require.Error(t, err)
	assert.Contains(t, err.Error(), `conflict at "globalTTL"`)
}

func TestLoadConfigSources_ScalarSameValueNoConflict(t *testing.T) {
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", "models:\n"+modelCfg("m1", "echo m1")+"\nglobalTTL: 100\n")
	writeYAML(t, dir, "b.yaml", "models:\n"+modelCfg("m2", "echo m2")+"\nglobalTTL: 100\n")

	cfg, err := LoadConfigSources("", dir)
	require.NoError(t, err)
	assert.Equal(t, 100, cfg.GlobalTTL)
}

func TestLoadConfigSources_MacrosConcatenate(t *testing.T) {
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", "macros:\n  LOW: 1\nmodels:\n"+modelCfg("m1", "echo ${LOW}"))
	writeYAML(t, dir, "b.yaml", "macros:\n  HIGH: 2\nmodels:\n"+modelCfg("m2", "echo ${HIGH}"))

	cfg, err := LoadConfigSources("", dir)
	require.NoError(t, err)
	// Both macros are available globally after merge.
	low, ok := cfg.Macros.Get("LOW")
	require.True(t, ok)
	assert.Equal(t, 1, low)
	high, ok := cfg.Macros.Get("HIGH")
	require.True(t, ok)
	assert.Equal(t, 2, high)
}

func TestLoadConfigSources_APIKeysConcatenate(t *testing.T) {
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", "models:\n"+modelCfg("m1", "echo m1")+"\napiKeys: [key-a]\n")
	writeYAML(t, dir, "b.yaml", "models:\n"+modelCfg("m2", "echo m2")+"\napiKeys: [key-b]\n")

	cfg, err := LoadConfigSources("", dir)
	require.NoError(t, err)
	assert.ElementsMatch(t, []string{"key-a", "key-b"}, cfg.RequiredAPIKeys)
}

func TestLoadConfigSources_RoutingGroupsMerge(t *testing.T) {
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", `
models:
`+modelCfg("m1", "echo m1")+`
routing:
  router:
    settings:
      groups:
        groupA:
          members: [m1]
`)
	writeYAML(t, dir, "b.yaml", `
models:
`+modelCfg("m2", "echo m2")+`
routing:
  router:
    settings:
      groups:
        groupB:
          members: [m2]
`)

	cfg, err := LoadConfigSources("", dir)
	require.NoError(t, err)
	groups := cfg.Routing.Router.Settings.Groups
	assert.Contains(t, groups, "groupA")
	assert.Contains(t, groups, "groupB")
	// default group added by pipeline for orphaned/leftover routing groups...
	// here both groups reference distinct models
}

func TestLoadConfigSources_EnvMacrosSubstituted(t *testing.T) {
	dir := t.TempDir()
	// Use ${PORT} in cmd so the pipeline allocates a port and substitutes it;
	// verifies env/macro substitution runs on the merged document.
	writeYAML(t, dir, "a.yaml", "models:\n  m1:\n    cmd: serve --port ${PORT}\n    proxy: \"http://localhost:${PORT}\"\n")
	cfg, err := LoadConfigSources("", dir)
	require.NoError(t, err)
	m := cfg.Models["m1"]
	assert.NotContains(t, m.Cmd, "${PORT}", "PORT macro should have been substituted")
	assert.NotContains(t, m.Proxy, "${PORT}", "PORT macro should have been substituted in proxy")
}

func TestLoadConfigSources_EnvMacroInFlowStyleList(t *testing.T) {
	// Regression: flow-style lists with ${env.*} must parse. Previously
	// parseSource unmarshalled before env substitution, so the brace in
	// [${env.API_KEY}] was misread as a flow mapping and parsing failed.
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", "models:\n  m1:\n    cmd: echo hi\n    proxy: \"http://localhost:9999\"\n")
	writeYAML(t, dir, "keys.yaml", "apiKeys: [${env.TEST_API_KEY}]\nmodels:\n  m2:\n    cmd: echo hi\n    proxy: \"http://localhost:9998\"\n")

	t.Setenv("TEST_API_KEY", "secret123")
	cfg, err := LoadConfigSources("", dir)
	require.NoError(t, err)
	assert.Contains(t, cfg.RequiredAPIKeys, "secret123")
}

func TestLoadConfigSources_SortedOrderDeterministic(t *testing.T) {
	// Two files defining distinct models, scanned in z..a order by filename.
	// Determine merged result is the same regardless of how the FS returns them.
	dir := t.TempDir()
	writeYAML(t, dir, "z.yaml", "models:\n"+modelCfg("zmodel", "echo z"))
	writeYAML(t, dir, "a.yaml", "models:\n"+modelCfg("amodel", "echo a"))

	const runs = 3
	for i := 0; i < runs; i++ {
		cfg, err := LoadConfigSources("", dir)
		require.NoError(t, err)
		// startPort-based allocation: first allocated model gets 5800.
		// Sorted order means amodel gets 5800, zmodel gets 5801.
		_, _, ok := cfg.FindConfig("amodel")
		assert.True(t, ok)
		_, _, ok = cfg.FindConfig("zmodel")
		assert.True(t, ok)
	}
}

func TestLoadConfigSources_EmptyDirWithConfig(t *testing.T) {
	dir := t.TempDir()
	cfgDir := t.TempDir()
	cfgPath := writeYAML(t, dir, "main.yaml", "models:\n"+modelCfg("m1", "echo m1"))

	cfg, err := LoadConfigSources(cfgPath, cfgDir)
	require.NoError(t, err)
	assert.Contains(t, cfg.Models, "m1")
}

func TestLoadConfigSources_EmptyDirOnly(t *testing.T) {
	// An empty -config-dir with no -config is an error: there is nothing to
	// load and silently producing an empty config would mask the misconfig.
	cfgDir := t.TempDir()
	_, err := LoadConfigSources("", cfgDir)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "no configuration sources found")
}

func TestLoadConfigSources_AssertNoUnknownMacrosAfterMerge(t *testing.T) {
	// Macros defined in one file should not satisfy unknown-macro validation in
	// another — they do, because merge concats global macros before validation
	// runs. This test documents that a macro from file A is usable in file B.
	dir := t.TempDir()
	writeYAML(t, dir, "macros.yaml", "macros:\n  SHARED: hello\nmodels:\n"+modelCfg("dummy", "echo dummy"))
	writeYAML(t, dir, "use.yaml", "models:\n"+modelCfg("user", "echo ${SHARED}"))

	cfg, err := LoadConfigSources("", dir)
	require.NoError(t, err)
	m := cfg.Models["user"]
	assert.Contains(t, m.Cmd, "hello")
	assert.NotContains(t, m.Cmd, "${SHARED}")
}

func TestLoadConfigSources_KindMismatchErrors(t *testing.T) {
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", "startPort: 5800\nmodels:\n"+modelCfg("m1", "echo m1"))
	writeYAML(t, dir, "b.yaml", "startPort: [5800, 5801]\nmodels:\n"+modelCfg("m2", "echo m2"))

	_, err := LoadConfigSources("", dir)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "incompatible YAML node kinds")
}

func TestLoadConfigSources_NullYieldsToValue(t *testing.T) {
	// File A: routing.router block absent (null on root for routing);
	// file B: defines routing.router.settings.groups. Merge should keep B's.
	dir := t.TempDir()
	writeYAML(t, dir, "a.yaml", "models:\n"+modelCfg("m1", "echo m1"))
	writeYAML(t, dir, "b.yaml", "routing:\n  router:\n    settings:\n      groups:\n        g1:\n          members: [m1]\nmodels:\n"+modelCfg("m2", "echo m2"))

	cfg, err := LoadConfigSources("", dir)
	require.NoError(t, err)
	assert.Contains(t, cfg.Routing.Router.Settings.Groups, "g1")
}
