package namespace

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

	"github.com/DeBrosOfficial/network/pkg/gateway"
	"go.uber.org/zap"
	"gopkg.in/yaml.v3"
)

// Bugboard #25 (warm reconcile) — gatewayWebRTCInSync decides whether a
// running namespace gateway's on-disk WebRTC block already matches the
// desired config. ReconcileGateway restarts the gateway ONLY when this
// returns false, so the function is the guard against both (a) leaving a
// drifted gateway broken and (b) restart-looping a correct one on every
// boot.

func desiredEnabled() gateway.InstanceConfig {
	return gateway.InstanceConfig{
		WebRTCEnabled: true,
		SFUPort:       30000,
		TURNDomain:    "turn.ns-anchat-test.orama-devnet.network",
		TURNSecret:    "the-secret",
	}
}

func TestGatewayWebRTCInSync_driftedBlockMissing_returnsFalse(t *testing.T) {
	// The exact bug-25 warm case: the running config has NO webrtc block
	// (enabled=false, port 0, empty secret) but the DB-desired config has
	// it enabled. MUST report out-of-sync so ReconcileGateway restarts.
	onDisk := gateway.GatewayYAMLWebRTC{} // zero value = no block
	if gatewayWebRTCInSync(onDisk, desiredEnabled()) {
		t.Fatal("BUG #25 REGRESSION: empty on-disk block vs DB-enabled desired must be out-of-sync (needs restart)")
	}
}

func TestGatewayWebRTCInSync_matchingBlock_returnsTrue(t *testing.T) {
	// After a reconcile fixes the config, the on-disk block matches the
	// desired. MUST report in-sync so the NEXT boot does NOT restart again
	// (no restart loop — this is why we compare the actual on-disk config
	// instead of the stale state file).
	onDisk := gateway.GatewayYAMLWebRTC{
		Enabled:    true,
		SFUPort:    30000,
		TURNDomain: "turn.ns-anchat-test.orama-devnet.network",
		TURNSecret: "the-secret",
	}
	if !gatewayWebRTCInSync(onDisk, desiredEnabled()) {
		t.Error("matching on-disk block must be in-sync (no restart) — else restart loop on every boot")
	}
}

func TestGatewayWebRTCInSync_eachFieldDriftDetected(t *testing.T) {
	// Any single drifted field must trigger a restart. Pins that the
	// comparison covers all five webrtc fields (a future refactor that
	// drops one would silently let that field drift forever).
	base := gateway.GatewayYAMLWebRTC{
		Enabled: true, SFUPort: 30000,
		TURNDomain: "turn.ns-anchat-test.orama-devnet.network", TURNSecret: "the-secret",
	}
	mutations := []struct {
		name string
		mut  func(w *gateway.GatewayYAMLWebRTC)
	}{
		{"enabled flipped off", func(w *gateway.GatewayYAMLWebRTC) { w.Enabled = false }},
		{"sfu port changed", func(w *gateway.GatewayYAMLWebRTC) { w.SFUPort = 30001 }},
		{"turn domain changed", func(w *gateway.GatewayYAMLWebRTC) { w.TURNDomain = "turn.other" }},
		{"turn secret rotated", func(w *gateway.GatewayYAMLWebRTC) { w.TURNSecret = "rotated" }},
		{"stealth domain changed", func(w *gateway.GatewayYAMLWebRTC) { w.TURNStealthDomain = "cdn-deadbeef0000.orama-devnet.network" }},
	}
	for _, tc := range mutations {
		t.Run(tc.name, func(t *testing.T) {
			d := base
			tc.mut(&d)
			if gatewayWebRTCInSync(d, desiredEnabled()) {
				t.Errorf("drift in %q not detected — gateway would keep serving stale config", tc.name)
			}
		})
	}
}

func TestGatewayWebRTCInSync_bothDisabled_returnsTrue(t *testing.T) {
	// A namespace genuinely without WebRTC: on-disk block empty, desired
	// disabled. In-sync → no restart. (Avoids churning non-webrtc
	// namespaces on every boot.)
	if !gatewayWebRTCInSync(gateway.GatewayYAMLWebRTC{}, gateway.InstanceConfig{}) {
		t.Error("disabled on-disk + disabled desired must be in-sync (no restart)")
	}
}

// Bugboard #837 follow-up (drift on the secrets encryption key) —
// gatewayConfigInSync extends the bug-25 WebRTC drift check with the
// serverless secrets key. A namespace gateway spawned before the key was
// plumbed has an empty on-disk key; once the desired key is non-empty we
// want a rewrite+restart so secrets management turns on. But both-empty must
// stay a no-op so non-secrets hosts don't restart-loop.

func TestGatewayConfigInSync_secretsKeyMissingOnDisk_returnsFalse(t *testing.T) {
	// On-disk YAML has no secrets key (pre-#837 gateway), desired has one.
	// MUST drift so ReconcileGateway rewrites + restarts to enable secrets.
	onDisk := gateway.GatewayYAMLConfig{} // empty secrets_encryption_key
	desired := gateway.InstanceConfig{SecretsEncryptionKey: "the-key"}
	if gatewayConfigInSync(onDisk, desired) {
		t.Fatal("empty on-disk secrets key vs non-empty desired must be out-of-sync (needs restart to enable secrets)")
	}
}

func TestGatewayConfigInSync_secretsKeyMatches_returnsTrue(t *testing.T) {
	// After a reconcile, on-disk key matches desired. MUST be in-sync so the
	// next boot does not restart again (no loop).
	onDisk := gateway.GatewayYAMLConfig{SecretsEncryptionKey: "the-key"}
	desired := gateway.InstanceConfig{SecretsEncryptionKey: "the-key"}
	if !gatewayConfigInSync(onDisk, desired) {
		t.Error("matching secrets key must be in-sync (no restart) — else restart loop on every boot")
	}
}

func TestGatewayConfigInSync_bothSecretsKeysEmpty_returnsTrue(t *testing.T) {
	// A host with no secrets key (empty desired) and an on-disk config also
	// without one MUST be in-sync — otherwise every boot would restart a
	// namespace gateway that legitimately has no secrets key.
	if !gatewayConfigInSync(gateway.GatewayYAMLConfig{}, gateway.InstanceConfig{}) {
		t.Error("empty on-disk + empty desired secrets key must be in-sync (no restart loop)")
	}
}

func TestGatewayConfigInSync_secretsKeyRotated_returnsFalse(t *testing.T) {
	// A rotated key (both non-empty but different) must drift so the rewrite
	// propagates the new key.
	onDisk := gateway.GatewayYAMLConfig{SecretsEncryptionKey: "old-key"}
	desired := gateway.InstanceConfig{SecretsEncryptionKey: "new-key"}
	if gatewayConfigInSync(onDisk, desired) {
		t.Error("rotated secrets key (old != new) must be out-of-sync")
	}
}

func TestGatewayConfigInSync_webrtcDriftStillDetected(t *testing.T) {
	// The combined check must not lose the bug-25 WebRTC surface: WebRTC
	// drift with matching (empty) secrets keys must still report out-of-sync.
	onDisk := gateway.GatewayYAMLConfig{WebRTC: gateway.GatewayYAMLWebRTC{}}
	desired := gateway.InstanceConfig{WebRTCEnabled: true, SFUPort: 30000}
	if gatewayConfigInSync(onDisk, desired) {
		t.Error("WebRTC drift must still be detected by the combined in-sync check")
	}
}

// ReconcileGateway I/O paths that DON'T restart (the restart path needs
// real systemd, so it's covered by the pure helper above). These pin
// that a matching config is a clean no-op and that an unreadable config
// surfaces an error instead of blind-restarting.

func writeGatewayConfig(t *testing.T, base, ns, nodeID string, wr gateway.GatewayYAMLWebRTC) {
	t.Helper()
	dir := filepath.Join(base, ns, "configs")
	if err := os.MkdirAll(dir, 0755); err != nil {
		t.Fatal(err)
	}
	b, _ := yaml.Marshal(gateway.GatewayYAMLConfig{ClientNamespace: ns, WebRTC: wr})
	if err := os.WriteFile(filepath.Join(dir, "gateway-"+nodeID+".yaml"), b, 0644); err != nil {
		t.Fatal(err)
	}
}

func TestReconcileGateway_inSyncIsNoOpNoError(t *testing.T) {
	base := t.TempDir()
	ns, node := "anchat-test", "node-1"
	writeGatewayConfig(t, base, ns, node, gateway.GatewayYAMLWebRTC{
		Enabled: true, SFUPort: 30000,
		TURNDomain: "turn.ns-anchat-test.orama-devnet.network", TURNSecret: "the-secret",
	})
	s := NewSystemdSpawner(base, "", zap.NewNop())

	// Desired == on-disk → must return nil WITHOUT attempting a restart
	// (RestartGateway would error here since there's no real systemd, so
	// a nil return proves we never reached it).
	err := s.ReconcileGateway(context.Background(), ns, node, desiredEnabled())
	if err != nil {
		t.Errorf("in-sync config must be a clean no-op; got %v (did it try to restart?)", err)
	}
}

func TestReconcileGateway_missingConfigReturnsErrorNotRestart(t *testing.T) {
	// No config file on disk → return an error so the caller leaves the
	// running gateway alone, rather than blind-restarting a healthy one.
	s := NewSystemdSpawner(t.TempDir(), "", zap.NewNop())
	err := s.ReconcileGateway(context.Background(), "anchat-test", "node-1", desiredEnabled())
	if err == nil {
		t.Error("missing config must return an error (don't blind-restart a healthy gateway)")
	}
}

func TestGatewayWebRTCInSync_stealthEnableDetectedAsDrift(t *testing.T) {
	// feat-124: enabling stealth must drift an otherwise-matching gateway so
	// the reconciler rewrites its yaml with turn_stealth_domain and restarts
	// it — that's how turn.credentials starts advertising turns:<host>:443.
	onDisk := gateway.GatewayYAMLWebRTC{
		Enabled: true, SFUPort: 30000,
		TURNDomain: "turn.ns-anchat-test.orama-devnet.network", TURNSecret: "the-secret",
	}
	desired := desiredEnabled()
	desired.TURNStealthDomain = "cdn-abc123def456.orama-devnet.network"
	if gatewayWebRTCInSync(onDisk, desired) {
		t.Error("stealth enable not detected as drift — gateway would never advertise the stealth URI")
	}

	// And once the yaml carries it, the same desired config is in-sync (no
	// restart loop).
	onDisk.TURNStealthDomain = desired.TURNStealthDomain
	if !gatewayWebRTCInSync(onDisk, desired) {
		t.Error("matching stealth domain reported as drift — restart loop")
	}
}
