package main

import (
	"encoding/json"
	"flag"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func resetConfigFlags(args []string) {
	flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
	os.Args = args
}

func applyMergeFromJSON(t *testing.T, dst *Config, payload string) {
	t.Helper()
	var incoming Config
	if err := json.Unmarshal([]byte(payload), &incoming); err != nil {
		t.Fatalf("unmarshal incoming: %v", err)
	}
	var present map[string]json.RawMessage
	if err := json.Unmarshal([]byte(payload), &present); err != nil {
		t.Fatalf("unmarshal present keys: %v", err)
	}
	mergeConfigExplicit(dst, &incoming, present)
}

func TestDefaultConfigServerDefaults(t *testing.T) {
	cfg := DefaultConfig()
	if cfg.Port != 8989 {
		t.Fatalf("port default: got %d", cfg.Port)
	}
	if cfg.Pool.Host != "pool.supportxmr.com" || cfg.Pool.Port != 443 || !cfg.Pool.UseTLS {
		t.Fatalf("pool defaults wrong: %+v", cfg.Pool)
	}
	if len(cfg.Pool.BackupPools) < 2 {
		t.Fatalf("expected TLS backup pools, got %+v", cfg.Pool.BackupPools)
	}
	if cfg.Server.PublicURL != "" {
		t.Fatalf("public_url should default empty, got %q", cfg.Server.PublicURL)
	}
	if cfg.Server.StatsRetentionHours != 168 {
		t.Fatalf("stats retention default: got %d", cfg.Server.StatsRetentionHours)
	}
	if cfg.Server.MaxAgents != 256 {
		t.Fatalf("max agents default: got %d", cfg.Server.MaxAgents)
	}
	if !cfg.Server.LogAgentConnections {
		t.Fatal("log_agent_connections should default true")
	}
	if cfg.Server.DashboardSubtitle != "security is just an emotion" {
		t.Fatalf("dashboard subtitle default wrong: %q", cfg.Server.DashboardSubtitle)
	}
	if cfg.DefaultAgent.Threads != 4 {
		t.Fatalf("default agent threads: got %d", cfg.DefaultAgent.Threads)
	}
}

func TestMergeConfigExplicitPortOnlyPreservesPoolTLS(t *testing.T) {
	dst := DefaultConfig()
	dst.Pool.UseTLS = true
	applyMergeFromJSON(t, dst, `{"port":9000}`)
	if dst.Port != 9000 {
		t.Fatalf("port not merged")
	}
	if !dst.Pool.UseTLS {
		t.Fatal("partial PUT with port only must not reset pool.use_tls")
	}
}

func TestMergeConfigExplicitNestedServerPartialPreservesBooleans(t *testing.T) {
	dst := DefaultConfig()
	dst.Server.LogAgentConnections = true
	dst.Server.OpenFirewallOnStart = true
	dst.Server.DashboardSubtitle = "keep-me"

	applyMergeFromJSON(t, dst, `{"server":{"dashboard_subtitle":"updated"}}`)

	if dst.Server.DashboardSubtitle != "updated" {
		t.Fatalf("subtitle not updated: %q", dst.Server.DashboardSubtitle)
	}
	if !dst.Server.LogAgentConnections {
		t.Fatal("nested partial PUT must not reset log_agent_connections")
	}
	if !dst.Server.OpenFirewallOnStart {
		t.Fatal("nested partial PUT must not reset open_firewall_on_start")
	}
}

func TestMergeConfigExplicitAllowsBlankPublicURL(t *testing.T) {
	dst := DefaultConfig()
	dst.Server.PublicURL = "https://stale.example.com"

	applyMergeFromJSON(t, dst, `{"server":{"public_url":""}}`)

	if dst.Server.PublicURL != "" {
		t.Fatalf("explicit blank public_url must clear old value, got %q", dst.Server.PublicURL)
	}
}

func TestMergeConfigExplicitNestedPoolPartialPreservesUseTLS(t *testing.T) {
	dst := DefaultConfig()
	dst.Pool.UseTLS = true
	dst.Pool.Host = "original.pool"

	applyMergeFromJSON(t, dst, `{"pool":{"host":"new.pool.example"}}`)

	if dst.Pool.Host != "new.pool.example" {
		t.Fatalf("host not updated: %q", dst.Pool.Host)
	}
	if !dst.Pool.UseTLS {
		t.Fatal("nested partial pool PUT must preserve use_tls")
	}
}

func TestMergeConfigExplicitExplicitBooleanFalse(t *testing.T) {
	dst := DefaultConfig()
	if !dst.Pool.UseTLS {
		t.Fatal("precondition: default pool TLS true")
	}

	applyMergeFromJSON(t, dst, `{"pool":{"use_tls":false}}`)
	if dst.Pool.UseTLS {
		t.Fatal("explicit use_tls:false must be applied")
	}
}

func TestMergeConfigExplicitAbsentSectionUntouched(t *testing.T) {
	dst := DefaultConfig()
	dst.Wallet.Address = "48keep"
	applyMergeFromJSON(t, dst, `{"port":7777}`)
	if dst.Wallet.Address != "48keep" {
		t.Fatalf("wallet should be untouched: %q", dst.Wallet.Address)
	}
}

func TestMergeConfigExplicitNilPresentKeysFallsBack(t *testing.T) {
	dst := DefaultConfig()
	dst.Pool.UseTLS = true
	src := &Config{Port: 9000}
	mergeConfigExplicit(dst, src, nil)
	if dst.Port != 9000 {
		t.Fatalf("port not merged via fallback")
	}
	// mergeConfig fallback still overwrites bools — documented legacy path
	if dst.Pool.UseTLS {
		t.Log("mergeConfig fallback resets UseTLS when presentKeys nil (LoadConfig uses mergeConfig)")
	}
}

func TestMergeConfigExplicitDefaultAgentConfigKey(t *testing.T) {
	dst := DefaultConfig()
	dst.DefaultAgent.AdaptToHardware = true
	dst.DefaultAgent.Threads = 8

	applyMergeFromJSON(t, dst, `{"default_agent_config":{"threads":2}}`)

	if dst.DefaultAgent.Threads != 2 {
		t.Fatalf("threads not updated: %d", dst.DefaultAgent.Threads)
	}
	if !dst.DefaultAgent.AdaptToHardware {
		t.Fatal("partial default_agent_config must preserve adapt_to_hardware")
	}
}

func TestMergeConfigExplicitBackgroundPartial(t *testing.T) {
	dst := DefaultConfig()
	dst.Background.AutoStart = true
	dst.Background.SilentMode = true

	applyMergeFromJSON(t, dst, `{"background":{"auto_start":false}}`)

	if dst.Background.AutoStart {
		t.Fatal("explicit auto_start:false must apply")
	}
	if !dst.Background.SilentMode {
		t.Fatal("partial background PUT must preserve silent_mode")
	}
}

func TestMergeConfigExplicitAlertsEmailEnabled(t *testing.T) {
	dst := DefaultConfig()
	dst.Alerts.EmailEnabled = false

	applyMergeFromJSON(t, dst, `{"alerts":{"email_enabled":true}}`)
	if !dst.Alerts.EmailEnabled {
		t.Fatal("explicit email_enabled:true must apply")
	}
}

func TestMergeConfigPreservesPoolTLSWhenPartialUpdate(t *testing.T) {
	dst := DefaultConfig()
	dst.Pool.UseTLS = true
	src := &Config{Port: 9000}
	mergeConfig(dst, src)
	if dst.Port != 9000 {
		t.Fatalf("port not merged")
	}
	// mergeConfig (file load path) still resets bools — known legacy behaviour
	if !dst.Pool.UseTLS {
		t.Log("mergeConfig resets UseTLS on partial update — file load uses mergeConfig not mergeConfigExplicit")
	}
}

func TestConfigJSONRoundTrip(t *testing.T) {
	cfg := DefaultConfig()
	cfg.Wallet.Address = "48testwallet"
	data, err := json.Marshal(cfg)
	if err != nil {
		t.Fatal(err)
	}
	var decoded Config
	if err := json.Unmarshal(data, &decoded); err != nil {
		t.Fatal(err)
	}
	if decoded.Wallet.Address != cfg.Wallet.Address {
		t.Fatalf("wallet mismatch")
	}
}

func TestDefaultConfigPort(t *testing.T) {
	cfg := DefaultConfig()
	if cfg.Port != 8989 {
		t.Fatalf("expected port 8989, got %d", cfg.Port)
	}
}

func TestLoadConfigFromFile(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "config.json")
	data, err := json.Marshal(DefaultConfig())
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(cfgPath, data, 0644); err != nil {
		t.Fatal(err)
	}
	raw, err := os.ReadFile(cfgPath)
	if err != nil {
		t.Fatal(err)
	}
	var loaded Config
	if err := json.Unmarshal(raw, &loaded); err != nil {
		t.Fatal(err)
	}
	if loaded.Port != 8989 {
		t.Fatalf("expected port 8989, got %d", loaded.Port)
	}
}

func TestConfigSaveAndReload(t *testing.T) {
	dir := t.TempDir()
	cfg := DefaultConfig()
	cfg.DataDir = dir
	cfg.Port = 7777
	cfg.Wallet.Address = "48savedwallet"
	cfg.Server.FleetSecret = "test-secret"

	if err := cfg.Save(); err != nil {
		t.Fatal(err)
	}

	raw, err := os.ReadFile(filepath.Join(dir, "config.json"))
	if err != nil {
		t.Fatal(err)
	}
	var loaded Config
	if err := json.Unmarshal(raw, &loaded); err != nil {
		t.Fatal(err)
	}
	if loaded.Port != 7777 {
		t.Fatalf("saved port: got %d", loaded.Port)
	}
	if loaded.Wallet.Address != "48savedwallet" {
		t.Fatalf("saved wallet: got %q", loaded.Wallet.Address)
	}
	if loaded.Server.FleetSecret != "test-secret" {
		t.Fatalf("saved fleet secret: got %q", loaded.Server.FleetSecret)
	}
}

func TestLoadConfigCLIFlags(t *testing.T) {
	dir := t.TempDir()
	resetConfigFlags([]string{"test", "-port", "9999", "-data", dir})

	cfg := LoadConfig()
	if cfg.Port != 9999 {
		t.Fatalf("CLI port: got %d", cfg.Port)
	}
	if cfg.DataDir != dir {
		t.Fatalf("CLI data dir: got %q", cfg.DataDir)
	}
}

func TestLoadConfigMergesFileOverrides(t *testing.T) {
	dir := t.TempDir()
	fileCfg := DefaultConfig()
	fileCfg.Port = 9001
	fileCfg.Pool.Host = "custom.pool.example"
	fileCfg.Wallet.Address = "48fromfile"
	data, err := json.Marshal(fileCfg)
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "config.json"), data, 0644); err != nil {
		t.Fatal(err)
	}

	resetConfigFlags([]string{"test", "-port", "8989", "-data", dir})
	cfg := LoadConfig()

	if cfg.Port != 9001 {
		t.Fatalf("file port override: got %d", cfg.Port)
	}
	if cfg.Pool.Host != "custom.pool.example" {
		t.Fatalf("file pool host: got %q", cfg.Pool.Host)
	}
	if cfg.Wallet.Address != "48fromfile" {
		t.Fatalf("file wallet: got %q", cfg.Wallet.Address)
	}
}

func TestLoadConfigLegacyOpenFirewallDefault(t *testing.T) {
	dir := t.TempDir()
	// Legacy config without open_firewall_on_start key — LoadConfig forces true.
	payload := `{"port":9100,"server":{"dashboard_subtitle":"legacy"}}`
	if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(payload), 0644); err != nil {
		t.Fatal(err)
	}

	resetConfigFlags([]string{"test", "-data", dir})
	cfg := LoadConfig()

	if !cfg.Server.OpenFirewallOnStart {
		t.Fatal("legacy config without open_firewall_on_start must default true")
	}
	if cfg.Server.DashboardSubtitle != "legacy" {
		t.Fatalf("subtitle from file: got %q", cfg.Server.DashboardSubtitle)
	}
}

func TestLoadConfigExplicitOpenFirewallFalse(t *testing.T) {
	dir := t.TempDir()
	payload := `{"server":{"open_firewall_on_start":false}}`
	if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(payload), 0644); err != nil {
		t.Fatal(err)
	}

	resetConfigFlags([]string{"test", "-data", dir})
	cfg := LoadConfig()

	if cfg.Server.OpenFirewallOnStart {
		t.Fatal("explicit open_firewall_on_start:false must be honored")
	}
}

func TestPoolURLTLS(t *testing.T) {
	cfg := DefaultConfig()
	got := cfg.PoolURL()
	want := "stratum+ssl://pool.supportxmr.com:443"
	if got != want {
		t.Fatalf("PoolURL TLS: got %q want %q", got, want)
	}
}

func TestPoolURLPlainTCP(t *testing.T) {
	cfg := DefaultConfig()
	cfg.Pool.UseTLS = false
	cfg.Pool.Port = 3333
	got := cfg.PoolURL()
	want := "stratum+tcp://pool.supportxmr.com:3333"
	if got != want {
		t.Fatalf("PoolURL plain: got %q want %q", got, want)
	}
}

func TestNestedJSONKeys(t *testing.T) {
	present := map[string]json.RawMessage{
		"pool": json.RawMessage(`{"host":"x","port":4444}`),
		"server": json.RawMessage(`invalid`),
	}
	poolKeys := nestedJSONKeys(present, "pool")
	if poolKeys == nil {
		t.Fatal("expected pool keys")
	}
	if _, ok := poolKeys["host"]; !ok {
		t.Fatal("missing host key")
	}
	if nestedJSONKeys(present, "missing") != nil {
		t.Fatal("missing section should return nil")
	}
	if nestedJSONKeys(present, "server") != nil {
		t.Fatal("invalid nested JSON should return nil")
	}
	if nestedJSONKeys(nil, "pool") != nil {
		t.Fatal("nil present map should return nil")
	}
}

func TestMergeConfigLegacyScalarsAndBooleans(t *testing.T) {
	dst := DefaultConfig()
	dst.Wallet.Address = "48original"
	dst.Pool.UseTLS = true
	dst.Background.SilentMode = true
	dst.Server.LogAgentConnections = true

	src := &Config{
		Port: 8080,
		Pool: PoolConfig{Host: "legacy.pool", Port: 4444, Password: "pw"},
		Wallet: WalletConfig{Address: "48new"},
		Background: BackgroundConfig{
			SilentMode: false,
			RunAs:      "user",
			AutoStart:  false,
		},
		Alerts: AlertsConfig{EmailEnabled: true},
		Server: ServerSettings{
			LogAgentConnections:    false,
			LogShareSubmissions:    true,
			OpenFirewallOnStart:    false,
			DashboardSubtitle:      "merged",
			StatsRetentionHours:    72,
		},
	}
	mergeConfig(dst, src)

	if dst.Port != 8080 {
		t.Fatalf("port: got %d", dst.Port)
	}
	if dst.Pool.Host != "legacy.pool" || dst.Pool.Port != 4444 {
		t.Fatalf("pool: %+v", dst.Pool)
	}
	if dst.Wallet.Address != "48new" {
		t.Fatalf("wallet: %q", dst.Wallet.Address)
	}
	if dst.Background.SilentMode {
		t.Fatal("mergeConfig must apply background.silent_mode false")
	}
	if dst.Background.RunAs != "user" {
		t.Fatalf("run_as: %q", dst.Background.RunAs)
	}
	if dst.Background.AutoStart {
		t.Fatal("auto_start false must apply")
	}
	if !dst.Alerts.EmailEnabled {
		t.Fatal("email_enabled true must apply")
	}
	if dst.Server.LogAgentConnections {
		t.Fatal("log_agent_connections false must apply")
	}
	if !dst.Server.LogShareSubmissions {
		t.Fatal("log_share_submissions true must apply")
	}
	if dst.Server.OpenFirewallOnStart {
		t.Fatal("open_firewall_on_start false must apply")
	}
	if dst.Server.DashboardSubtitle != "merged" {
		t.Fatalf("subtitle: %q", dst.Server.DashboardSubtitle)
	}
	if dst.Server.StatsRetentionHours != 72 {
		t.Fatalf("stats retention: %d", dst.Server.StatsRetentionHours)
	}
	// Known legacy: UseTLS copied from src even when false on zero-value partial src
	if dst.Pool.UseTLS {
		t.Log("mergeConfig sets UseTLS from src zero value on partial update")
	}
}

func TestMergeConfigLegacyDefaultAgentBools(t *testing.T) {
	dst := DefaultConfig()
	dst.DefaultAgent.AdaptToHardware = true
	dst.DefaultAgent.FileLogging = true
	dst.DefaultAgent.StealthMode = false

	src := &Config{
		DefaultAgent: AgentDefaults{
			StealthMode: true,
		},
	}
	mergeConfig(dst, src)

	if !dst.DefaultAgent.StealthMode {
		t.Fatal("stealth_mode true must apply via bool branch")
	}
	if dst.DefaultAgent.AdaptToHardware {
		t.Fatal("adapt_to_hardware should follow src false on stealth_mode branch")
	}
	if dst.DefaultAgent.FileLogging {
		t.Fatal("file_logging should follow src false on stealth_mode branch")
	}
}

func TestMergeConfigLegacyPoolUseTLSFalse(t *testing.T) {
	dst := DefaultConfig()
	dst.Pool.UseTLS = true
	src := &Config{Pool: PoolConfig{Host: "only-host"}}
	mergeConfig(dst, src)
	if dst.Pool.UseTLS {
		t.Fatal("mergeConfig always copies UseTLS from src; zero src clears TLS")
	}
	if dst.Pool.Host != "only-host" {
		t.Fatalf("host: %q", dst.Pool.Host)
	}
}

func TestMergeConfigLegacyZeroValuesPreserveScalars(t *testing.T) {
	dst := DefaultConfig()
	dst.Port = 8989
	dst.Pool.Port = 3333
	src := &Config{}
	mergeConfig(dst, src)
	if dst.Port != 8989 {
		t.Fatalf("zero src port must not overwrite: got %d", dst.Port)
	}
	if dst.Pool.Port != 3333 {
		t.Fatalf("zero src pool port must not overwrite: got %d", dst.Pool.Port)
	}
}

func TestLoadConfigIgnoresInvalidJSONFile(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{broken`), 0644); err != nil {
		t.Fatal(err)
	}
	resetConfigFlags([]string{"test", "-port", "8888", "-data", dir})
	cfg := LoadConfig()
	if cfg.Port != 8888 {
		t.Fatalf("invalid file should fall back to CLI port: got %d", cfg.Port)
	}
}

func TestLoadConfigOpenFirewallKeyDetection(t *testing.T) {
	withKey := `{"server":{"open_firewall_on_start":false}}`
	if strings.Contains(withKey, `"open_firewall_on_start"`) != true {
		t.Fatal("test precondition")
	}
	withoutKey := `{"server":{"dashboard_subtitle":"x"}}`
	if strings.Contains(withoutKey, `"open_firewall_on_start"`) {
		t.Fatal("test precondition")
	}
}
