package config

import (
	"errors"
	"reflect"
	"strings"
	"testing"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)

func TestPrecedenceFileValuesOverrideBuiltInDefaults(t *testing.T) {
	cfg := applyFileConfig(t, `version: 4
concurrency:
  total_llm: 4
  stage_workers:
    extract: 2
output:
  directory: ./file-output
cache:
  chunk_plans:
    directory: ./file-plans
    mode: refresh
  checkpoints:
    directory: ./file-checkpoints
debug:
  directory: ./file-debug
`)
	if cfg.Concurrency.TotalLLM != 4 || cfg.Concurrency.StageWorkers["extract"] != 2 ||
		cfg.Output.Directory != "./file-output" || cfg.Cache.ChunkPlans.Directory != "file-plans" ||
		cfg.Cache.ChunkPlans.Mode != pipeline.ChunkCacheRefresh || cfg.Cache.Checkpoints.Directory != "file-checkpoints" ||
		cfg.Debug.Directory != "./file-debug" {
		t.Fatalf("file values did not override defaults: %#v", cfg)
	}
}

func TestPrecedenceOperationalEnvironmentOverridesFileValues(t *testing.T) {
	cfg := applyFileConfig(t, `version: 4
concurrency:
  total_llm: 2
  stage_workers:
    extract: 1
output:
  directory: ./file-output
cache:
  chunk_plans:
    directory: ./file-plans
    mode: refresh
  checkpoints:
    directory: ./file-checkpoints
debug:
  directory: ./file-debug
`)
	env := map[string]string{
		"NOTARIUS_TOTAL_LLM_CONCURRENCY":  "8",
		"NOTARIUS_STAGE_WORKERS_EXTRACT":  "6",
		"NOTARIUS_OUTPUT_DIR":             "/env/output",
		"NOTARIUS_CACHE_CHUNK_PLANS_MODE": "bypass",
		"NOTARIUS_CACHE_CHUNK_PLANS_DIR":  "/env/plans",
		"NOTARIUS_CACHE_CHECKPOINTS_DIR":  "/env/checkpoints",
		"NOTARIUS_DEBUG_DIR":              "/env/debug",
	}
	if err := cfg.ApplyEnvOverridesWithLookup(lookupValues(env)); err != nil {
		t.Fatal(err)
	}
	if cfg.Concurrency.TotalLLM != 8 || cfg.Concurrency.StageWorkers["extract"] != 6 ||
		cfg.Output.Directory != "/env/output" || cfg.Cache.ChunkPlans.Directory != "/env/plans" ||
		cfg.Cache.ChunkPlans.Mode != pipeline.ChunkCacheBypass || cfg.Cache.Checkpoints.Directory != "/env/checkpoints" ||
		cfg.Debug.Directory != "/env/debug" {
		t.Fatalf("environment values did not override file values: %#v", cfg)
	}
}

func TestPrecedenceExtractWorkersFollowEffectiveConcurrencyUnlessExplicit(t *testing.T) {
	tests := []struct {
		name       string
		file       string
		env        map[string]string
		wantTotal  int
		wantWorker int
	}{
		{
			name:       "default follows environment total",
			file:       "version: 4\n",
			env:        map[string]string{"NOTARIUS_TOTAL_LLM_CONCURRENCY": "5"},
			wantTotal:  5,
			wantWorker: 5,
		},
		{
			name:       "file worker is retained",
			file:       "version: 4\nconcurrency:\n  total_llm: 3\n  stage_workers:\n    extract: 2\n",
			env:        map[string]string{"NOTARIUS_TOTAL_LLM_CONCURRENCY": "6"},
			wantTotal:  6,
			wantWorker: 2,
		},
		{
			name: "environment worker is retained",
			file: "version: 4\nconcurrency:\n  total_llm: 2\n",
			env: map[string]string{
				"NOTARIUS_TOTAL_LLM_CONCURRENCY": "6",
				"NOTARIUS_STAGE_WORKERS_EXTRACT": "4",
			},
			wantTotal:  6,
			wantWorker: 4,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := applyFileConfig(t, tt.file)
			if err := cfg.ApplyEnvOverridesWithLookup(lookupValues(tt.env)); err != nil {
				t.Fatal(err)
			}
			if cfg.Concurrency.TotalLLM != tt.wantTotal || cfg.Concurrency.StageWorkers["extract"] != tt.wantWorker {
				t.Fatalf("concurrency = %#v, want total %d and extract %d", cfg.Concurrency, tt.wantTotal, tt.wantWorker)
			}
		})
	}
}

func TestPrecedenceEmptyFileCacheDirectoriesDeferPerUserResolution(t *testing.T) {
	cfg := applyFileConfig(t, `version: 4
cache:
  chunk_plans:
    directory: ""
  checkpoints:
    directory: ""
`)
	if err := cfg.Validate(); err != nil {
		t.Fatalf("empty file cache directories should be valid: %v", err)
	}
	if cfg.Cache.ChunkPlans.Directory != "" || cfg.Cache.Checkpoints.Directory != "" {
		t.Fatalf("empty cache directories were not preserved for deferred resolution: %#v", cfg.Cache)
	}
	resolver := func() (string, error) { return "/user/cache", nil }
	chunkPlans, err := DefaultChunkPlanRoot(resolver)
	if err != nil {
		t.Fatal(err)
	}
	checkpoints, err := DefaultCheckpointRoot(resolver)
	if err != nil {
		t.Fatal(err)
	}
	if chunkPlans != "/user/cache/notarius/chunk-plans" || checkpoints != "/user/cache/notarius/checkpoints" {
		t.Fatalf("deferred cache roots = %q, %q", chunkPlans, checkpoints)
	}
}

func TestDefaultCacheRootsRejectInvalidUserCacheResolvers(t *testing.T) {
	tests := []struct {
		name     string
		resolver func() (string, error)
		want     string
	}{
		{name: "nil resolver", want: "must not be nil"},
		{
			name: "resolver failure",
			resolver: func() (string, error) {
				return "", errors.New("cache home unavailable")
			},
			want: "resolve user cache directory",
		},
		{name: "empty directory", resolver: func() (string, error) { return "  ", nil }, want: "must not be empty"},
	}
	families := []struct {
		name string
		root func(func() (string, error)) (string, error)
	}{
		{name: "chunk plans", root: DefaultChunkPlanRoot},
		{name: "checkpoints", root: DefaultCheckpointRoot},
	}

	for _, family := range families {
		for _, tt := range tests {
			t.Run(family.name+"/"+tt.name, func(t *testing.T) {
				_, err := family.root(tt.resolver)
				if err == nil || !strings.Contains(err.Error(), tt.want) {
					t.Fatalf("error = %v, want substring %q", err, tt.want)
				}
			})
		}
	}
}

func TestEnvEmptyDirectoryOverridesAreErrors(t *testing.T) {
	tests := []string{
		"NOTARIUS_OUTPUT_DIR",
		"NOTARIUS_CACHE_CHUNK_PLANS_DIR",
		"NOTARIUS_CACHE_CHECKPOINTS_DIR",
		"NOTARIUS_DEBUG_DIR",
	}
	for _, name := range tests {
		t.Run(name, func(t *testing.T) {
			cfg := Default()
			err := cfg.ApplyEnvOverridesWithLookup(lookupValues(map[string]string{name: " \t"}))
			if err == nil || !strings.Contains(err.Error(), name) {
				t.Fatalf("error = %v, want responsible environment variable", err)
			}
		})
	}
}

func TestEnvInvalidIntegersAndChunkCacheModesReportTheirNames(t *testing.T) {
	tests := map[string]string{
		"NOTARIUS_TOTAL_LLM_CONCURRENCY":  "not-an-integer",
		"NOTARIUS_STAGE_WORKERS_EXTRACT":  "not-an-integer",
		"NOTARIUS_CACHE_CHUNK_PLANS_MODE": "not-a-cache-mode",
	}
	for name, value := range tests {
		t.Run(name, func(t *testing.T) {
			cfg := Default()
			err := cfg.ApplyEnvOverridesWithLookup(lookupValues(map[string]string{name: value}))
			if err == nil || !strings.Contains(err.Error(), name) {
				t.Fatalf("error = %v, want responsible environment variable", err)
			}
		})
	}
}

func TestEnvRemovedProviderVariablesAreIgnored(t *testing.T) {
	before := Default()
	cfg := Default()
	removed := map[string]string{
		"NOTARIUS_LLM_DEFAULT_ENDPOINT": "ignored-provider-setting",
		"NOTARIUS_LLM_DEFAULT_MODEL":    "ignored-provider-setting",
	}
	if err := cfg.ApplyEnvOverridesWithLookup(lookupValues(removed)); err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(cfg, before) {
		t.Fatalf("removed provider variables changed configuration: %#v", cfg)
	}
}

func lookupValues(values map[string]string) func(string) (string, bool) {
	return func(name string) (string, bool) {
		value, ok := values[name]
		return value, ok
	}
}
