package config

import (
	"os"
	"strings"
	"testing"
	"time"
)

// unset removes an env key for the duration of the test, restoring it after.
// Needed to observe a default for a key read with LookupEnv (where present-empty
// means "explicitly disabled", not "use default").
func unset(t *testing.T, key string) {
	t.Helper()
	if old, ok := os.LookupEnv(key); ok {
		t.Cleanup(func() {
			if err := os.Setenv(key, old); err != nil {
				t.Fatalf("restore %s: %v", key, err)
			}
		})
	}
	if err := os.Unsetenv(key); err != nil {
		t.Fatalf("unset %s: %v", key, err)
	}
}

// setEnv sets env vars for the test and clears them afterward, so cases don't
// leak into one another. t.Setenv handles restoration.
func setEnv(t *testing.T, kv map[string]string) {
	t.Helper()
	for k, v := range kv {
		t.Setenv(k, v)
	}
}

func TestLoad_AppliesDefaults(t *testing.T) {
	// Clear the ones with defaults so we observe the fallback, not the host env.
	setEnv(t, map[string]string{
		"TAPIR_GATEWAY_URL":         "",
		"TAPIR_SUMMARIZER_MODEL":    "",
		"TAPIR_YT_TOKEN_REF":        "",
		"TAPIR_OAUTH_REDIRECT_ADDR": "",
		"TAPIR_SUMMARIZER_TIMEOUT":  "",
		"TAPIR_POLL_INTERVAL":       "",
	})

	c, err := Load()
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if c.GatewayURL != defaultGatewayURL {
		t.Errorf("GatewayURL = %q, want default %q", c.GatewayURL, defaultGatewayURL)
	}
	if c.SummarizerModel != defaultSummarizerModel {
		t.Errorf("SummarizerModel = %q, want default %q", c.SummarizerModel, defaultSummarizerModel)
	}
	if c.YTTokenRef != defaultYTTokenRef {
		t.Errorf("YTTokenRef = %q, want default %q", c.YTTokenRef, defaultYTTokenRef)
	}
	if c.SummarizerTimeout != defaultSummarizerTimeout {
		t.Errorf("SummarizerTimeout = %v, want default %v", c.SummarizerTimeout, defaultSummarizerTimeout)
	}
	if c.PollInterval != 0 {
		t.Errorf("PollInterval = %v, want 0 (run once)", c.PollInterval)
	}
	if c.FetchBackoff != defaultFetchBackoff {
		t.Errorf("FetchBackoff = %v, want default %v", c.FetchBackoff, defaultFetchBackoff)
	}
	if c.AutoSummarizeWindow != defaultAutoSummarizeWindow {
		t.Errorf("AutoSummarizeWindow = %v, want default %v", c.AutoSummarizeWindow, defaultAutoSummarizeWindow)
	}
}

func TestLoad_SummarizerChainDefaults(t *testing.T) {
	setEnv(t, map[string]string{
		"TAPIR_SUMMARIZER_MODEL":     "",
		"TAPIR_SUMMARY_MAX_TOKENS":   "",
		"TAPIR_MAX_TRANSCRIPT_CHARS": "",
	})
	unset(t, "TAPIR_FALLBACK_MODEL")
	unset(t, "TAPIR_CLOUD_FALLBACK_MODEL")

	c, err := Load()
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if c.FallbackModel != defaultFallbackModel {
		t.Errorf("FallbackModel = %q, want %q", c.FallbackModel, defaultFallbackModel)
	}
	if c.CloudFallbackModel != defaultCloudFallbackModel {
		t.Errorf("CloudFallbackModel = %q, want %q", c.CloudFallbackModel, defaultCloudFallbackModel)
	}
	if c.SummaryMaxTokens != defaultSummaryMaxTokens {
		t.Errorf("SummaryMaxTokens = %d, want %d", c.SummaryMaxTokens, defaultSummaryMaxTokens)
	}
	if c.MaxTranscriptChars != defaultMaxTranscriptChars {
		t.Errorf("MaxTranscriptChars = %d, want %d", c.MaxTranscriptChars, defaultMaxTranscriptChars)
	}
}

// An explicitly empty cloud-fallback env disables external routing — the lever a
// client deployment pulls so content never leaves the local stack.
func TestLoad_EmptyCloudFallbackDisables(t *testing.T) {
	t.Setenv("TAPIR_CLOUD_FALLBACK_MODEL", "")
	c, err := Load()
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if c.CloudFallbackModel != "" {
		t.Errorf("CloudFallbackModel = %q, want empty (disabled)", c.CloudFallbackModel)
	}
}

func TestLoad_OnboardSummarizeCount(t *testing.T) {
	cases := []struct {
		name, env string
		want      int
	}{
		{"default", "", defaultOnboardSummarizeCount},
		{"explicit", "4", 4},
		{"zero disables", "0", 0},
		{"clamped to hard cap", "50", maxOnboardSummarizeCount},
		{"negative clamps to zero", "-3", 0},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			setEnv(t, map[string]string{"TAPIR_ONBOARD_SUMMARIZE_COUNT": c.env})
			cfg, err := Load()
			if err != nil {
				t.Fatalf("Load: %v", err)
			}
			if cfg.OnboardSummarizeCount != c.want {
				t.Fatalf("OnboardSummarizeCount = %d, want %d", cfg.OnboardSummarizeCount, c.want)
			}
		})
	}
}

func TestLoad_OnboardSummarizeCountInvalid(t *testing.T) {
	setEnv(t, map[string]string{"TAPIR_ONBOARD_SUMMARIZE_COUNT": "three"})
	if _, err := Load(); err == nil {
		t.Fatal("Load: want error for non-numeric TAPIR_ONBOARD_SUMMARIZE_COUNT")
	}
}

func TestLoad_ParsesValues(t *testing.T) {
	setEnv(t, map[string]string{
		"TAPIR_USER_ID":               "11111111-1111-1111-1111-111111111111",
		"TAPIR_GATEWAY_URL":           "http://example/v1",
		"TAPIR_GATEWAY_KEY":           "sk-test",
		"TAPIR_SUMMARIZER_MODEL":      "iguana/deepseek-r1-14b",
		"TAPIR_SUMMARIZER_TIMEOUT":    "90s",
		"TAPIR_DB_DSN":                "postgres://x",
		"TAPIR_POLL_INTERVAL":         "10m",
		"TAPIR_FETCH_BACKOFF":         "30m",
		"TAPIR_AUTO_SUMMARIZE_WINDOW": "48h",
	})

	c, err := Load()
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if c.UserID != "11111111-1111-1111-1111-111111111111" {
		t.Errorf("UserID = %q", c.UserID)
	}
	if c.GatewayKey != "sk-test" {
		t.Errorf("GatewayKey = %q", c.GatewayKey)
	}
	if c.SummarizerModel != "iguana/deepseek-r1-14b" {
		t.Errorf("SummarizerModel = %q", c.SummarizerModel)
	}
	if c.SummarizerTimeout != 90*time.Second {
		t.Errorf("SummarizerTimeout = %v, want 90s", c.SummarizerTimeout)
	}
	if c.PollInterval != 10*time.Minute {
		t.Errorf("PollInterval = %v, want 10m", c.PollInterval)
	}
	if c.FetchBackoff != 30*time.Minute {
		t.Errorf("FetchBackoff = %v, want 30m", c.FetchBackoff)
	}
	if c.AutoSummarizeWindow != 48*time.Hour {
		t.Errorf("AutoSummarizeWindow = %v, want 48h", c.AutoSummarizeWindow)
	}
}

func TestLoad_RejectsBadDuration(t *testing.T) {
	setEnv(t, map[string]string{"TAPIR_SUMMARIZER_TIMEOUT": "not-a-duration"})
	if _, err := Load(); err == nil {
		t.Fatal("want error on unparsable duration, got nil")
	}
}

func TestValidateForRun_ReportsMissing(t *testing.T) {
	c := Config{} // nothing set
	err := c.ValidateForRun()
	if err == nil {
		t.Fatal("want error when required run fields are missing")
	}
	for _, want := range []string{"TAPIR_USER_ID", "TAPIR_DB_DSN", "TAPIR_YT_CLIENT_ID"} {
		if !strings.Contains(err.Error(), want) {
			t.Errorf("error %q must name missing field %q", err, want)
		}
	}
}

func TestValidateForAuth_PassesWhenComplete(t *testing.T) {
	c := Config{
		YTClientID:     "id",
		YTClientSecret: "secret",
		YTTokenRef:     "youtube/refresh_token",
		SecretsFile:    "/tmp/secrets.json",
	}
	if err := c.ValidateForAuth(); err != nil {
		t.Errorf("ValidateForAuth: unexpected error %v", err)
	}
}

func TestLoad_OnboardSummarizerModel(t *testing.T) {
	cases := []struct {
		name, env string
		set       bool
		want      string
	}{
		{"default", "", false, defaultOnboardSummarizerModel},
		{"explicit", "koala/some-model", true, "koala/some-model"},
		{"empty disables (collapses to shared processor)", "", true, ""},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			env := map[string]string{}
			if c.set {
				env["TAPIR_ONBOARD_SUMMARIZER_MODEL"] = c.env
			}
			setEnv(t, env)
			cfg, err := Load()
			if err != nil {
				t.Fatalf("Load: %v", err)
			}
			if cfg.OnboardSummarizerModel != c.want {
				t.Fatalf("OnboardSummarizerModel = %q, want %q", cfg.OnboardSummarizerModel, c.want)
			}
		})
	}
}

func TestLoad_OnboardMaxVideoSeconds(t *testing.T) {
	cases := []struct {
		name, env string
		want      int
	}{
		{"default", "", defaultOnboardMaxVideoSeconds},
		{"explicit", "7200", 7200},
		{"zero disables", "0", 0},
		{"negative clamps to zero", "-9", 0},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			setEnv(t, map[string]string{"TAPIR_ONBOARD_MAX_VIDEO_SECONDS": c.env})
			cfg, err := Load()
			if err != nil {
				t.Fatalf("Load: %v", err)
			}
			if cfg.OnboardMaxVideoSeconds != c.want {
				t.Fatalf("OnboardMaxVideoSeconds = %d, want %d", cfg.OnboardMaxVideoSeconds, c.want)
			}
		})
	}
}

func TestLoad_OnboardMaxVideoSecondsInvalid(t *testing.T) {
	setEnv(t, map[string]string{"TAPIR_ONBOARD_MAX_VIDEO_SECONDS": "long"})
	if _, err := Load(); err == nil {
		t.Fatal("Load: want error for non-numeric TAPIR_ONBOARD_MAX_VIDEO_SECONDS")
	}
}
