package tests

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

	"github.com/dirstral/dir2mcp/internal/config"
	"github.com/dirstral/dir2mcp/tests/testutil"
)

func TestLoadFile_ReadsYAMLValues(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, ".dir2mcp.yaml")
	writeFile(t, path, ""+
		"root_dir: ./repo\n"+
		"state_dir: ./repo/.state\n"+
		"listen_addr: 127.0.0.1:7000\n"+
		"mcp_path: /custom\n"+
		"public: true\n"+
		"auth_mode: none\n"+
		"allowed_origins:\n"+
		"  - https://example.com\n")

	cfg, err := config.LoadFile(path)
	if err != nil {
		t.Fatalf("LoadFile failed: %v", err)
	}
	if cfg.RootDir != "./repo" {
		t.Fatalf("RootDir=%q want=%q", cfg.RootDir, "./repo")
	}
	if cfg.StateDir != "./repo/.state" {
		t.Fatalf("StateDir=%q want=%q", cfg.StateDir, "./repo/.state")
	}
	if cfg.ListenAddr != "127.0.0.1:7000" {
		t.Fatalf("ListenAddr=%q want=%q", cfg.ListenAddr, "127.0.0.1:7000")
	}
	if cfg.MCPPath != "/custom" {
		t.Fatalf("MCPPath=%q want=%q", cfg.MCPPath, "/custom")
	}
	if !cfg.Public {
		t.Fatal("expected Public=true")
	}
	if cfg.AuthMode != "none" {
		t.Fatalf("AuthMode=%q want=%q", cfg.AuthMode, "none")
	}
	if len(cfg.AllowedOrigins) != 1 || cfg.AllowedOrigins[0] != "https://example.com" {
		t.Fatalf("AllowedOrigins=%v want=[https://example.com]", cfg.AllowedOrigins)
	}
}

func TestLoad_FileThenEnvOverridesYAML(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, ".dir2mcp.yaml")
	// The Mistral flat keys / DIR2MCP_* model env vars were removed in
	// the provider clean break (#38); docling_command remains a flat
	// YAML key whose DIR2MCP_DOCLING_COMMAND env override still applies,
	// so it exercises the same file-then-env precedence path.
	writeFile(t, path, "docling_command: yaml-docling {input}\n")

	testutil.WithWorkingDir(t, tmp, func() {
		t.Setenv("DIR2MCP_DOCLING_COMMAND", "env-docling {input}")

		cfg, err := config.Load(".dir2mcp.yaml")
		if err != nil {
			t.Fatalf("Load failed: %v", err)
		}
		if cfg.DoclingCommand != "env-docling {input}" {
			t.Fatalf("DoclingCommand=%q want env override of YAML", cfg.DoclingCommand)
		}
	})
}

func TestSaveFile_WritesNonSecretYAML(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, ".dir2mcp.yaml")

	cfg := config.Default()
	cfg.RootDir = "/tmp/repo"
	cfg.StateDir = "/tmp/repo/.dir2mcp"
	cfg.DoclingCommand = "docling --to md - {input}"
	cfg.ElevenLabsAPIKey = "another-secret"
	cfg.AllowedOrigins = []string{"http://localhost", "https://example.com"}

	if err := config.SaveFile(path, cfg); err != nil {
		t.Fatalf("SaveFile failed: %v", err)
	}

	raw, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("ReadFile failed: %v", err)
	}
	text := string(raw)
	if !strings.Contains(text, "root_dir: /tmp/repo") {
		t.Fatalf("saved yaml missing root_dir, got:\n%s", text)
	}
	if !strings.Contains(text, "docling_command:") {
		t.Fatalf("saved yaml missing docling_command, got:\n%s", text)
	}
	if strings.Contains(strings.ToLower(text), "elevenlabs_api_key") {
		t.Fatalf("saved yaml must not include the ELEVENLABS_API_KEY key, got:\n%s", text)
	}
	if strings.Contains(text, "another-secret") {
		t.Fatalf("saved yaml must not include secret values, got:\n%s", text)
	}
}

// TestSaveFile_WatchSettingsRoundTrip guards against the persisted watch keys
// being written under a name the loader does not recognize: a SaveFile -> Load
// cycle must preserve IngestWatch / IngestWatchDebounce.
func TestSaveFile_WatchSettingsRoundTrip(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, ".dir2mcp.yaml")

	cfg := config.Default()
	cfg.RootDir = "/tmp/repo"
	cfg.StateDir = "/tmp/repo/.dir2mcp"
	cfg.IngestWatch = true
	cfg.IngestWatchDebounce = 1500 * time.Millisecond
	cfg.IngestDoclingServeURL = "http://127.0.0.1:5001"

	if err := config.SaveFile(path, cfg); err != nil {
		t.Fatalf("SaveFile failed: %v", err)
	}

	loaded, err := config.LoadFile(path)
	if err != nil {
		t.Fatalf("LoadFile failed: %v", err)
	}
	if !loaded.IngestWatch {
		t.Errorf("IngestWatch did not round-trip: got false, want true")
	}
	if loaded.IngestWatchDebounce != 1500*time.Millisecond {
		t.Errorf("IngestWatchDebounce did not round-trip: got %v, want 1.5s", loaded.IngestWatchDebounce)
	}
	if loaded.IngestDoclingServeURL != "http://127.0.0.1:5001" {
		t.Errorf("IngestDoclingServeURL did not round-trip: got %q", loaded.IngestDoclingServeURL)
	}
}

func TestSaveFile_RejectsInvalidConfig(t *testing.T) {
	tests := []struct {
		name string
		cfg  config.Config
	}{
		{
			name: "negative-inactivity",
			cfg: func() config.Config {
				c := config.Default()
				c.SessionInactivityTimeout = -1 * time.Second
				return c
			}(),
		},
		{
			name: "max-lifetime-shorter-than-inactivity",
			cfg: func() config.Config {
				c := config.Default()
				c.SessionInactivityTimeout = 10 * time.Minute
				c.SessionMaxLifetime = 5 * time.Minute
				return c
			}(),
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			tmp := t.TempDir()
			path := filepath.Join(tmp, ".dir2mcp.yaml")
			cfg := tc.cfg
			if err := config.SaveFile(path, cfg); err == nil {
				t.Fatal("expected error saving invalid config, got nil")
			} else if !strings.Contains(err.Error(), "validate config") {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}

func TestLoadFile_MalformedYAMLReturnsError(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, ".dir2mcp.yaml")
	writeFile(t, path, "root_dir: [unterminated\n")

	_, err := config.LoadFile(path)
	if err == nil {
		t.Fatal("expected LoadFile to fail on malformed YAML")
	}
	if !strings.Contains(err.Error(), "parse config file") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestLoadFile_ReadsNestedSpecStyleKeys(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, ".dir2mcp.yaml")
	writeFile(t, path, ""+
		"rag:\n"+
		"  system_prompt: use citations\\nline2\n"+
		"  max_context_chars: 12345\n"+
		"  oversample_factor: 7\n"+
		"ingest:\n"+
		"  gitignore: false\n"+
		"  extractor: docling\n"+
		"  docling:\n"+
		"    command: docling --to md --output - {input}\n"+
		"    serve_url: http://127.0.0.1:5001\n"+
		"  pdf:\n"+
		"    mode: ocr\n"+
		"  images:\n"+
		"    mode: ocr_auto\n"+
		"  audio:\n"+
		"    mode: auto\n"+
		"  archives:\n"+
		"    mode: deep\n"+
		"  follow_symlinks: true\n"+
		"  max_file_mb: 42\n"+
		"  watch: true\n"+
		"  watch_debounce: 2s\n"+
		"stt:\n"+
		"  provider: elevenlabs\n"+
		"  mistral:\n"+
		"    model: voxtral-large-latest\n"+
		"  elevenlabs:\n"+
		"    model: scribe_v2\n"+
		"    language_code: tr\n"+
		"server:\n"+
		"  tls:\n"+
		"    cert_file: /tmp/cert.pem\n"+
		"    key_file: /tmp/key.pem\n"+
		"secrets:\n"+
		"  elevenlabs_api_key: nested-eleven\n"+
		"  x402_facilitator_url: https://facilitator.example\n")

	cfg, err := config.LoadFile(path)
	if err != nil {
		t.Fatalf("LoadFile failed: %v", err)
	}
	assertNestedRAGFields(t, cfg)
	assertNestedIngestFields(t, cfg)
	assertNestedSTTFields(t, cfg)
	assertNestedServerAndSecrets(t, cfg)
}

func assertNestedRAGFields(t *testing.T, cfg config.Config) {
	t.Helper()
	if cfg.RAGSystemPrompt != "use citations\nline2" {
		t.Fatalf("RAGSystemPrompt=%q", cfg.RAGSystemPrompt)
	}
	if cfg.RAGMaxContextChars != 12345 {
		t.Fatalf("RAGMaxContextChars=%d", cfg.RAGMaxContextChars)
	}
	if cfg.RAGOversampleFactor != 7 {
		t.Fatalf("RAGOversampleFactor=%d", cfg.RAGOversampleFactor)
	}
}

func assertNestedIngestFields(t *testing.T, cfg config.Config) {
	t.Helper()
	if cfg.IngestGitignore {
		t.Fatal("expected IngestGitignore=false")
	}
	if !cfg.IngestFollowSymlinks {
		t.Fatal("expected IngestFollowSymlinks=true")
	}
	if cfg.IngestMaxFileMB != 42 {
		t.Fatalf("IngestMaxFileMB=%d", cfg.IngestMaxFileMB)
	}
	if cfg.IngestPDFMode != "ocr" || cfg.IngestImagesMode != "ocr_auto" || cfg.IngestAudioMode != "auto" || cfg.IngestArchivesMode != "deep" {
		t.Fatalf("unexpected ingest mode values: pdf=%q images=%q audio=%q archives=%q", cfg.IngestPDFMode, cfg.IngestImagesMode, cfg.IngestAudioMode, cfg.IngestArchivesMode)
	}
	if cfg.IngestExtractor != "docling" {
		t.Fatalf("IngestExtractor=%q", cfg.IngestExtractor)
	}
	if cfg.DoclingCommand != "docling --to md --output - {input}" {
		t.Fatalf("DoclingCommand=%q", cfg.DoclingCommand)
	}
	if cfg.IngestDoclingServeURL != "http://127.0.0.1:5001" {
		t.Fatalf("IngestDoclingServeURL=%q", cfg.IngestDoclingServeURL)
	}
	if !cfg.IngestWatch {
		t.Fatal("expected IngestWatch=true")
	}
	if cfg.IngestWatchDebounce != 2*time.Second {
		t.Fatalf("IngestWatchDebounce=%v", cfg.IngestWatchDebounce)
	}
}

func assertNestedSTTFields(t *testing.T, cfg config.Config) {
	t.Helper()
	if cfg.STTProvider != "elevenlabs" {
		t.Fatalf("STTProvider=%q", cfg.STTProvider)
	}
	if cfg.STTMistralModel != "voxtral-large-latest" {
		t.Fatalf("STTMistralModel=%q", cfg.STTMistralModel)
	}
	if cfg.STTElevenLabsModel != "scribe_v2" {
		t.Fatalf("STTElevenLabsModel=%q", cfg.STTElevenLabsModel)
	}
	if cfg.STTElevenLabsLanguageCode != "tr" {
		t.Fatalf("STTElevenLabsLanguageCode=%q", cfg.STTElevenLabsLanguageCode)
	}
}

func assertNestedServerAndSecrets(t *testing.T, cfg config.Config) {
	t.Helper()
	if cfg.ServerTLSCertFile != "/tmp/cert.pem" {
		t.Fatalf("ServerTLSCertFile=%q", cfg.ServerTLSCertFile)
	}
	if cfg.ServerTLSKeyFile != "/tmp/key.pem" {
		t.Fatalf("ServerTLSKeyFile=%q", cfg.ServerTLSKeyFile)
	}
	if cfg.ElevenLabsAPIKey != "nested-eleven" {
		t.Fatalf("ElevenLabsAPIKey=%q", cfg.ElevenLabsAPIKey)
	}
	if cfg.X402.FacilitatorURL != "https://facilitator.example" {
		t.Fatalf("X402.FacilitatorURL=%q", cfg.X402.FacilitatorURL)
	}
}

func TestLoadFile_FlatAliasesRemainSupportedForNestedFields(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, ".dir2mcp.yaml")
	writeFile(t, path, ""+
		"rag_system_prompt: flat prompt\n"+
		"max_context_chars: 999\n"+
		"oversample_factor: 3\n"+
		"ingest_gitignore: false\n"+
		"ingest_pdf_mode: auto\n"+
		"ingest_images_mode: ocr_on\n"+
		"ingest_audio_mode: on\n"+
		"ingest_archives_mode: shallow\n"+
		"follow_symlinks: true\n"+
		"max_file_mb: 10\n"+
		"stt_provider: mistral\n"+
		"stt_mistral_model: voxtral-mini-latest\n"+
		"stt_elevenlabs_model: scribe\n"+
		"elevenlabs_language_code: en\n"+
		"secrets.elevenlabs_api_key: flat-eleven\n"+
		"secrets.x402_facilitator_url: https://flat-facilitator.example\n"+
		"tls_cert_file: /etc/cert.pem\n"+
		"tls_key_file: /etc/key.pem\n")

	cfg, err := config.LoadFile(path)
	if err != nil {
		t.Fatalf("LoadFile failed: %v", err)
	}
	assertFlatAliasGroups(t, cfg)
}

func assertFlatAliasGroups(t *testing.T, cfg config.Config) {
	t.Helper()
	assertFlatRAGAliases(t, cfg)
	assertFlatIngestAliases(t, cfg)
	assertFlatSTTAliases(t, cfg)
	assertFlatSecretsAndTLSAliases(t, cfg)
}

func assertFlatRAGAliases(t *testing.T, cfg config.Config) {
	t.Helper()
	if cfg.RAGSystemPrompt != "flat prompt" || cfg.RAGMaxContextChars != 999 || cfg.RAGOversampleFactor != 3 {
		t.Fatalf("rag aliases not applied: %+v", cfg)
	}
}

func assertFlatIngestAliases(t *testing.T, cfg config.Config) {
	t.Helper()
	if cfg.IngestGitignore || !cfg.IngestFollowSymlinks || cfg.IngestMaxFileMB != 10 {
		t.Fatalf("ingest aliases not applied: gitignore=%v follow=%v max=%d", cfg.IngestGitignore, cfg.IngestFollowSymlinks, cfg.IngestMaxFileMB)
	}
	if cfg.IngestPDFMode != "auto" || cfg.IngestImagesMode != "ocr_on" || cfg.IngestAudioMode != "on" || cfg.IngestArchivesMode != "shallow" {
		t.Fatalf("ingest mode aliases not applied: pdf=%q images=%q audio=%q archives=%q", cfg.IngestPDFMode, cfg.IngestImagesMode, cfg.IngestAudioMode, cfg.IngestArchivesMode)
	}
}

func assertFlatSTTAliases(t *testing.T, cfg config.Config) {
	t.Helper()
	if cfg.STTProvider != "mistral" || cfg.STTMistralModel != "voxtral-mini-latest" || cfg.STTElevenLabsModel != "scribe" || cfg.STTElevenLabsLanguageCode != "en" {
		t.Fatalf("stt aliases not applied: %+v", cfg)
	}
}

func assertFlatSecretsAndTLSAliases(t *testing.T, cfg config.Config) {
	t.Helper()
	if cfg.ElevenLabsAPIKey != "flat-eleven" {
		t.Fatalf("secret alias not applied: eleven=%q", cfg.ElevenLabsAPIKey)
	}
	if cfg.X402.FacilitatorURL != "https://flat-facilitator.example" {
		t.Fatalf("x402 facilitator alias not applied: %q", cfg.X402.FacilitatorURL)
	}
	if cfg.ServerTLSCertFile != "/etc/cert.pem" || cfg.ServerTLSKeyFile != "/etc/key.pem" {
		t.Fatalf("tls aliases not applied: cert=%q key=%q", cfg.ServerTLSCertFile, cfg.ServerTLSKeyFile)
	}
}

func TestLoadFile_LastValueWinsAcrossAliasForms(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, ".dir2mcp.yaml")
	writeFile(t, path, ""+
		"rag_max_context_chars: 100\n"+
		"rag:\n"+
		"  max_context_chars: 200\n")

	cfg, err := config.LoadFile(path)
	if err != nil {
		t.Fatalf("LoadFile failed: %v", err)
	}
	if cfg.RAGMaxContextChars != 200 {
		t.Fatalf("RAGMaxContextChars=%d want=200", cfg.RAGMaxContextChars)
	}
}
