package config

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

	"github.com/spf13/viper"
)

// minimalValidYAML is the smallest config that passes validateConfig.
// It only sets the fields that validateConfig strictly requires, leaving
// everything else at its viper default so we can assert default values.
const minimalValidYAML = `
webserver:
  listen_addr: "0.0.0.0"
  listen_port: 8080
  full_url: "/full-stream"
  lite_url: "/"
  domains_only_url: "/domains-only"
`

// writeConfigFile writes content to a temporary YAML file and returns its path.
func writeConfigFile(t *testing.T, content string) string {
	t.Helper()
	path := filepath.Join(t.TempDir(), "config.yaml")
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("failed to write temp config file: %v", err)
	}
	return path
}

// TestWebserverDefaults uses a config file that only sets required
// webserver fields so that optional keys still reflect their defaults.
func TestWebserverDefaults(t *testing.T) {
	// Only listen_addr and listen_port are set. Remaining keys should be default.
	yaml := `
webserver:
  listen_addr: "0.0.0.0"
  listen_port: 8080
`
	configPath := writeConfigFile(t, yaml)
	v := initViper(configPath)

	cases := []struct {
		key  string
		want interface{}
	}{
		{"webserver.full_url", "/full-stream"},
		{"webserver.lite_url", "/"},
		{"webserver.domains_only_url", "/domains-only"},
		{"webserver.real_ip", false},
		{"webserver.compression_enabled", false},
	}

	for _, testcase := range cases {
		switch want := testcase.want.(type) {
		case string:
			if got := v.GetString(testcase.key); got != want {
				t.Errorf("key %s: want %q, got %q", testcase.key, want, got)
			}
		case bool:
			if got := v.GetBool(testcase.key); got != want {
				t.Errorf("key %s: want %v, got %v", testcase.key, want, got)
			}
		}
	}
}

// TestPrometheusDefaults uses a config file that only sets required
// webserver fields so that optional keys still reflect their defaults.
func TestPrometheusDefaults(t *testing.T) {
	// No prometheus section in the config – all keys should come from defaults.
	configPath := writeConfigFile(t, minimalValidYAML)
	v := initViper(configPath)

	if got := v.GetBool("prometheus.enabled"); got != false {
		t.Errorf("prometheus.enabled: want false, got %v", got)
	}
	if got := v.GetString("prometheus.listen_addr"); got != "0.0.0.0" {
		t.Errorf("prometheus.listen_addr: want '0.0.0.0', got %q", got)
	}
	if got := v.GetInt("prometheus.listen_port"); got != 9090 {
		t.Errorf("prometheus.listen_port: want 9090, got %d", got)
	}
	if got := v.GetString("prometheus.metrics_url"); got != "/metrics" {
		t.Errorf("prometheus.metrics_url: want '/metrics', got %q", got)
	}
	if got := v.GetBool("prometheus.expose_system_metrics"); got != false {
		t.Errorf("prometheus.expose_system_metrics: want false, got %v", got)
	}
	if got := v.GetBool("prometheus.real_ip"); got != false {
		t.Errorf("prometheus.real_ip: want false, got %v", got)
	}
}

// TestGeneralDefaults uses an empty string as configPath.
// It tests whether the defaults are properly configured.
func TestGeneralDefaults(t *testing.T) {
	// No general section in the config – all keys should come from defaults.

	v := initViper("")

	if got := v.GetBool("general.disable_default_logs"); got != false {
		t.Errorf("general.disable_default_logs: want false, got %v", got)
	}
	if got := v.GetInt("general.buffer_sizes.websocket"); got != 300 {
		t.Errorf("general.buffer_sizes.websocket: want 300, got %d", got)
	}
	if got := v.GetInt("general.buffer_sizes.ctlog"); got != 1000 {
		t.Errorf("general.buffer_sizes.ctlog: want 1000, got %d", got)
	}
	if got := v.GetInt("general.buffer_sizes.broadcastmanager"); got != 10000 {
		t.Errorf("general.buffer_sizes.broadcastmanager: want 10000, got %d", got)
	}
	if got := v.GetBool("general.drop_old_logs"); got != true {
		t.Errorf("general.drop_old_logs: want true, got %v", got)
	}
	if got := v.GetBool("general.recovery.enabled"); got != false {
		t.Errorf("general.recovery.enabled: want false, got %v", got)
	}
	if got := v.GetString("general.recovery.ct_index_file"); got != "./ct_index.json" {
		t.Errorf("general.recovery.ct_index_file: want './ct_index.json', got %q", got)
	}
}

// TestConfigFileOverridesDefaults verifies that values from the config file override
// the defaults, while keys absent from the file still return their default values.
func TestConfigFileOverridesDefaults(t *testing.T) {
	yaml := `
webserver:
  listen_addr: "127.0.0.1"
  listen_port: 9999
`
	configPath := writeConfigFile(t, yaml)
	v := initViper(configPath)

	if got := v.GetString("webserver.listen_addr"); got != "127.0.0.1" {
		t.Errorf("listen_addr: want '127.0.0.1', got %q", got)
	}
	if got := v.GetInt("webserver.listen_port"); got != 9999 {
		t.Errorf("listen_port: want 9999, got %d", got)
	}

	// Keys absent from the file must still return the registered defaults.
	if got := v.GetString("webserver.full_url"); got != "/full-stream" {
		t.Errorf("full_url (default): want '/full-stream', got %q", got)
	}
}

// TestEnvOverridesDefaults verifies that environment variables
// take precedence over defaults when no config file is provided.
func TestEnvOverridesDefaults(t *testing.T) {
	t.Setenv("CERTSTREAM_WEBSERVER_LISTEN_PORT", "6543")

	configPath := writeConfigFile(t, minimalValidYAML)
	v := initViper(configPath)

	if got := v.GetInt("webserver.listen_port"); got != 6543 {
		t.Errorf("listen_port via env: want 6543, got %d", got)
	}
}

// TestEnvOverridesConfigFile verifies that environment variables
// take precedence over config file values.
func TestEnvOverridesConfigFile(t *testing.T) {
	configPath := writeConfigFile(t, minimalValidYAML)
	t.Setenv("CERTSTREAM_WEBSERVER_LISTEN_PORT", "7777")

	v := initViper(configPath)

	if got := v.GetInt("webserver.listen_port"); got != 7777 {
		t.Errorf("listen_port via env: want 7777, got %d", got)
	}
}

// TestEnvOverridesPrometheusPort verifies that environment variables
// can override config file values for the prometheus.listen_port key.
func TestEnvOverridesPrometheusPort(t *testing.T) {
	configPath := writeConfigFile(t, minimalValidYAML)
	t.Setenv("CERTSTREAM_PROMETHEUS_LISTEN_PORT", "19090")

	v := initViper(configPath)

	if got := v.GetInt("prometheus.listen_port"); got != 19090 {
		t.Errorf("prometheus.listen_port via env: want 19090, got %d", got)
	}
}

// viperFromYAML creates a fully initialized *viper.Viper from an in-memory
// YAML string by writing it to a temp file and calling initViper.  This
// exercises the same code path as production (defaults + file merge) and
// correctly handles the yaml:",inline" embedded struct tags via mapstructure.
func viperFromYAML(t *testing.T, content string) *viper.Viper {
	t.Helper()
	return initViper(writeConfigFile(t, content))
}

// validViperInstance returns a viper instance built from minimalValidYAML.
func validViperInstance(t *testing.T) *viper.Viper {
	t.Helper()
	return viperFromYAML(t, minimalValidYAML)
}

// TestLoadConfigFromViper_UnmarshalsWebserver tests if the nested Webserver
// struct is unmarshalled properly.
func TestLoadConfigFromViper_UnmarshalsWebserver(t *testing.T) {
	v := validViperInstance(t)
	cfg, err := loadConfigFromViper(v)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if cfg.Webserver.ListenAddr != "0.0.0.0" {
		t.Errorf("ListenAddr: want '0.0.0.0', got %q", cfg.Webserver.ListenAddr)
	}
	if cfg.Webserver.ListenPort != 8080 {
		t.Errorf("ListenPort: want 8080, got %d", cfg.Webserver.ListenPort)
	}
	if cfg.Webserver.FullURL != "/full-stream" {
		t.Errorf("FullURL: want '/full-stream', got %q", cfg.Webserver.FullURL)
	}
	if cfg.Webserver.LiteURL != "/" {
		t.Errorf("LiteURL: want '/', got %q", cfg.Webserver.LiteURL)
	}
	if cfg.Webserver.DomainsOnlyURL != "/domains-only" {
		t.Errorf("DomainsOnlyURL: want '/domains-only', got %q", cfg.Webserver.DomainsOnlyURL)
	}
}

// TestLoadConfigFromViper_UnmarshalsBufferSizes tests if the nested BufferSizes
// struct is unmarshalled properly.
func TestLoadConfigFromViper_UnmarshalsBufferSizes(t *testing.T) {
	v := viperFromYAML(t, `
webserver:
  listen_addr: "0.0.0.0"
  listen_port: 8080
  full_url: "/full-stream"
  lite_url: "/"
  domains_only_url: "/domains-only"
general:
  buffer_sizes:
    websocket: 512
    ctlog: 2048
    broadcastmanager: 4096
`)
	cfg, err := loadConfigFromViper(v)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if cfg.General.BufferSizes.Websocket != 512 {
		t.Errorf("BufferSizes.Websocket: want 512, got %d", cfg.General.BufferSizes.Websocket)
	}
	if cfg.General.BufferSizes.CTLog != 2048 {
		t.Errorf("BufferSizes.CTLog: want 2048, got %d", cfg.General.BufferSizes.CTLog)
	}
	if cfg.General.BufferSizes.BroadcastManager != 4096 {
		t.Errorf("BufferSizes.BroadcastManager: want 4096, got %d", cfg.General.BufferSizes.BroadcastManager)
	}
}

// TestLoadConfigFromViper_SetsAppConfig verifies that loadConfigFromViper updates
// the global AppConfig variable to match the loaded configuration.
func TestLoadConfigFromViper_SetsAppConfig(t *testing.T) {
	v := validViperInstance(t)
	cfg, err := loadConfigFromViper(v)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if AppConfig.Webserver.ListenPort != cfg.Webserver.ListenPort {
		t.Errorf("AppConfig.Webserver.ListenPort not synced: want %d, got %d",
			cfg.Webserver.ListenPort, AppConfig.Webserver.ListenPort)
	}
	if AppConfig.Webserver.ListenAddr != cfg.Webserver.ListenAddr {
		t.Errorf("AppConfig.Webserver.ListenAddr not synced: want %q, got %q",
			cfg.Webserver.ListenAddr, AppConfig.Webserver.ListenAddr)
	}
}

// TestLoadConfigFromViper_RecoverySettings tests if the nested BufferSizes
// struct is unmarshalled properly.
func TestLoadConfigFromViper_RecoverySettings(t *testing.T) {
	v := viperFromYAML(t, `
webserver:
  listen_addr: "0.0.0.0"
  listen_port: 8080
  full_url: "/full-stream"
  lite_url: "/"
  domains_only_url: "/domains-only"
general:
  recovery:
    enabled: true
    ct_index_file: "/tmp/my_index.json"
`)
	cfg, err := loadConfigFromViper(v)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if !cfg.General.Recovery.Enabled {
		t.Errorf("Recovery.Enabled: want true, got false")
	}
	if cfg.General.Recovery.CTIndexFile != "/tmp/my_index.json" {
		t.Errorf("Recovery.CTIndexFile: want '/tmp/my_index.json', got %q", cfg.General.Recovery.CTIndexFile)
	}
}

func TestReadConfigViper_MinimalValidFile(t *testing.T) {
	configPath := writeConfigFile(t, minimalValidYAML)

	cfg, err := ReadConfig(configPath)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if cfg.Webserver.ListenAddr != "0.0.0.0" {
		t.Errorf("ListenAddr: want '0.0.0.0', got %q", cfg.Webserver.ListenAddr)
	}
	if cfg.Webserver.ListenPort != 8080 {
		t.Errorf("ListenPort: want 8080, got %d", cfg.Webserver.ListenPort)
	}
}

func TestReadConfigViper_PrometheusSection(t *testing.T) {
	yaml := minimalValidYAML + `
prometheus:
  enabled: true
  listen_addr: "0.0.0.0"
  listen_port: 9090
  metrics_url: "/metrics"
  expose_system_metrics: true
`
	configPath := writeConfigFile(t, yaml)

	cfg, err := ReadConfig(configPath)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if !cfg.Prometheus.Enabled {
		t.Errorf("Prometheus.Enabled: want true, got false")
	}
	if cfg.Prometheus.ListenPort != 9090 {
		t.Errorf("Prometheus.ListenPort: want 9090, got %d", cfg.Prometheus.ListenPort)
	}
	if cfg.Prometheus.MetricsURL != "/metrics" {
		t.Errorf("Prometheus.MetricsURL: want '/metrics', got %q", cfg.Prometheus.MetricsURL)
	}
	if !cfg.Prometheus.ExposeSystemMetrics {
		t.Errorf("Prometheus.ExposeSystemMetrics: want true, got false")
	}
}

func TestReadConfigViper_CustomBufferSizes(t *testing.T) {
	yaml := minimalValidYAML + `
general:
  buffer_sizes:
    websocket: 500
    ctlog: 2000
    broadcastmanager: 20000
`
	configPath := writeConfigFile(t, yaml)

	cfg, err := ReadConfig(configPath)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if cfg.General.BufferSizes.Websocket != 500 {
		t.Errorf("BufferSizes.Websocket: want 500, got %d", cfg.General.BufferSizes.Websocket)
	}
	if cfg.General.BufferSizes.CTLog != 2000 {
		t.Errorf("BufferSizes.CTLog: want 2000, got %d", cfg.General.BufferSizes.CTLog)
	}
	if cfg.General.BufferSizes.BroadcastManager != 20000 {
		t.Errorf("BufferSizes.BroadcastManager: want 20000, got %d", cfg.General.BufferSizes.BroadcastManager)
	}
}

func TestReadConfigViper_AdditionalLogs(t *testing.T) {
	yaml := minimalValidYAML + `
general:
  additional_logs:
    - url: "https://ct.example.com/log"
      operator: "Example"
      description: "Example CT log"
`
	configPath := writeConfigFile(t, yaml)

	cfg, err := ReadConfig(configPath)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if len(cfg.General.AdditionalLogs) != 1 {
		t.Fatalf("AdditionalLogs: want 1 entry, got %d", len(cfg.General.AdditionalLogs))
	}
	if cfg.General.AdditionalLogs[0].URL != "https://ct.example.com/log" {
		t.Errorf("AdditionalLogs[0].URL: want 'https://ct.example.com/log', got %q",
			cfg.General.AdditionalLogs[0].URL)
	}
	if cfg.General.AdditionalLogs[0].Operator != "Example" {
		t.Errorf("AdditionalLogs[0].Operator: want 'Example', got %q",
			cfg.General.AdditionalLogs[0].Operator)
	}
}

func TestReadConfigViper_InvalidAdditionalLogURLIgnored(t *testing.T) {
	yaml := minimalValidYAML + `
general:
  additional_logs:
    - url: "not-a-valid-url"
      operator: "Bad"
      description: "Invalid log"
    - url: "https://ct.example.com/valid-log"
      operator: "Good"
      description: "Valid log"
`
	configPath := writeConfigFile(t, yaml)

	cfg, err := ReadConfig(configPath)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if len(cfg.General.AdditionalLogs) != 1 {
		t.Errorf("AdditionalLogs: want 1 valid entry (invalid filtered), got %d", len(cfg.General.AdditionalLogs))
	}
}

func TestReadConfigViper_RecoveryConfig(t *testing.T) {
	yaml := minimalValidYAML + `
general:
  recovery:
    enabled: true
    ct_index_file: "./my_index.json"
`
	configPath := writeConfigFile(t, yaml)

	cfg, err := ReadConfig(configPath)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if !cfg.General.Recovery.Enabled {
		t.Errorf("Recovery.Enabled: want true, got false")
	}
	if cfg.General.Recovery.CTIndexFile != "./my_index.json" {
		t.Errorf("Recovery.CTIndexFile: want './my_index.json', got %q", cfg.General.Recovery.CTIndexFile)
	}
}
