package config

import (
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

func writeConfig(t *testing.T, path, body string) {
	t.Helper()
	if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
		t.Fatal(err)
	}
}

func TestLoadDirectoryAndDurations(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.yaml")
	writeConfig(t, path, "server:\n  http:\n    addr: :9000\n    timeout: 600s\ndata:\n  redis:\n    read_timeout: 200ms\nadmin:\n  jwt:\n    expires_time: 168h\n  media:\n    session_ttl: 36\n")
	loaded, err := Load(dir)
	if err != nil {
		t.Fatal(err)
	}
	if loaded.Server == nil || loaded.Server.HTTP == nil || loaded.Server.HTTP.Timeout != 10*time.Minute {
		t.Fatalf("server timeout = %#v", loaded.Server)
	}
	if loaded.Data == nil || loaded.Data.Redis == nil || loaded.Data.Redis.ReadTimeout != 200*time.Millisecond {
		t.Fatalf("redis timeout = %#v", loaded.Data)
	}
	if loaded.Admin == nil || loaded.Admin.JWT == nil || loaded.Admin.JWT.ExpiresTime != 7*24*time.Hour {
		t.Fatalf("jwt duration = %#v", loaded.Admin)
	}
	if loaded.Admin.ConfigPath != path {
		t.Fatalf("config path = %q, want %q", loaded.Admin.ConfigPath, path)
	}
}

func TestLoadRejectsInvalidDuration(t *testing.T) {
	path := filepath.Join(t.TempDir(), "config.yaml")
	writeConfig(t, path, "server:\n  http:\n    timeout: definitely-not-a-duration\n")
	if _, err := Load(path); err == nil {
		t.Fatal("invalid duration was accepted")
	}
}

func TestLoadEnvironmentOverride(t *testing.T) {
	path := filepath.Join(t.TempDir(), "config.yaml")
	writeConfig(t, path, "server:\n  http:\n    addr: :8000\n")
	t.Setenv("KRA_SERVER_HTTP_ADDR", ":9100")
	loaded, err := Load(path)
	if err != nil {
		t.Fatal(err)
	}
	if loaded.Server == nil || loaded.Server.HTTP == nil || loaded.Server.HTTP.Addr != ":9100" {
		t.Fatalf("environment override = %#v", loaded.Server)
	}
}

func TestStoreSnapshotIsolationAndSubscriptions(t *testing.T) {
	store := NewStore(&Config{Admin: &Admin{JWT: &JWT{SigningKey: "before"}}})
	var calls atomic.Int32
	stop := store.Subscribe(func(value *Config) {
		calls.Add(1)
		value.Admin.JWT.SigningKey = "callback-mutation"
	})
	store.Replace(&Config{Admin: &Admin{JWT: &JWT{SigningKey: "after"}}})
	stop()
	stop()
	value := store.Snapshot()
	if value == nil || value.Admin == nil || value.Admin.JWT == nil || value.Admin.JWT.SigningKey != "after" {
		t.Fatalf("snapshot was mutated by callback: %#v", value)
	}
	if calls.Load() != 1 {
		t.Fatalf("callback count = %d", calls.Load())
	}
}

func TestStoreConcurrentReadersAndReplacements(t *testing.T) {
	store := NewStore(&Config{Data: &Data{Database: &Database{Name: "initial"}}})
	var wait sync.WaitGroup
	for i := 0; i < 8; i++ {
		wait.Add(1)
		go func() {
			defer wait.Done()
			for j := 0; j < 500; j++ {
				_ = store.Snapshot()
			}
		}()
	}
	for i := 0; i < 500; i++ {
		store.Replace(&Config{Data: &Data{Database: &Database{Name: "next"}}})
	}
	wait.Wait()
}

func TestStoreWatchReloadAndClose(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.yaml")
	writeConfig(t, path, "admin:\n  app:\n    app_id: first\n")
	store, err := LoadStore(path)
	if err != nil {
		t.Fatal(err)
	}
	defer store.Close()
	writeConfig(t, path, "admin:\n  app:\n    app_id: second\n")
	deadline := time.Now().Add(3 * time.Second)
	for time.Now().Before(deadline) {
		value := store.Snapshot()
		if value != nil && value.Admin != nil && value.Admin.App != nil && value.Admin.App.AppID == "second" {
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatalf("watcher did not reload configuration: %#v", store.Snapshot())
}

func TestStoreWatchPreservesDatabaseBackedIntegrationSettings(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.yaml")
	writeConfig(t, path, "data: {}\nadmin: {}\n")
	store, err := LoadStore(path)
	if err != nil {
		t.Fatal(err)
	}
	defer store.Close()
	current := store.Snapshot()
	current.Admin.Storage = &Storage{Type: "local"}
	current.Admin.Email = &Email{Host: "smtp.example.com"}
	store.Replace(current)

	writeConfig(t, path, "data:\n  redis:\n    addr: 127.0.0.1:6379\nadmin: {}\n")
	deadline := time.Now().Add(3 * time.Second)
	for time.Now().Before(deadline) {
		value := store.Snapshot()
		if value != nil && value.Data != nil && value.Data.Redis != nil && value.Data.Redis.Addr != "" {
			if value.Admin == nil || value.Admin.Storage == nil || value.Admin.Storage.Type != "local" || value.Admin.Email == nil || value.Admin.Email.Host != "smtp.example.com" {
				t.Fatalf("database-backed settings were lost: %#v", value.Admin)
			}
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatal("watcher did not publish the changed file")
}

func TestMergeRuntimeConfigPreservesOmittedSections(t *testing.T) {
	current := &Config{
		Server: &Server{HTTP: &ServerHTTP{Addr: ":8080"}},
		Data:   &Data{Database: &Database{Source: "current-dsn"}},
		Admin:  &Admin{ConfigPath: "config.yaml", Storage: &Storage{Type: "local"}, Email: &Email{Host: "smtp.example.com"}},
	}
	next := &Config{Admin: &Admin{App: &App{AppID: "next"}}}
	merged := MergeRuntimeConfig(current, next)
	if merged.Server == nil || merged.Server.HTTP == nil || merged.Server.HTTP.Addr != ":8080" {
		t.Fatalf("server was not preserved: %#v", merged.Server)
	}
	if merged.Data == nil || merged.Data.Database == nil || merged.Data.Database.Source != "current-dsn" {
		t.Fatalf("data was not preserved: %#v", merged.Data)
	}
	if merged.Admin.Storage == nil || merged.Admin.Storage.Type != "local" || merged.Admin.Email == nil || merged.Admin.Email.Host != "smtp.example.com" {
		t.Fatalf("integration settings were not preserved: %#v", merged.Admin)
	}
	merged.Admin.Storage.Type = "changed"
	if current.Admin.Storage.Type != "local" {
		t.Fatal("MergeRuntimeConfig leaked mutable storage state into current config")
	}
}

func TestMergeRuntimeConfigPreservesOmittedNestedDataSections(t *testing.T) {
	current := &Config{Data: &Data{Database: &Database{Source: "current-dsn"}}}
	next := &Config{Data: &Data{}}
	merged := MergeRuntimeConfig(current, next)
	if merged.Data == nil || merged.Data.Database == nil || merged.Data.Database.Source != "current-dsn" {
		t.Fatalf("nested database section was not preserved: %#v", merged.Data)
	}
}
