package config

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

func TestDefaultValidate(t *testing.T) {
	if err := Default().Validate(); err != nil {
		t.Fatalf("default invalid: %v", err)
	}
}

func TestLoadAndValidate(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.json")
	os.WriteFile(path, []byte(`{"audio":{"toneLeftHz":900,"toneRightHz":1700,"toneAmplitude":0.3},"fm":{"frequencyMHz":99.9},"backend":{"kind":"file","outputPath":"out.f32"},"control":{"listenAddress":"127.0.0.1:8088"}}`), 0o644)
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	if cfg.Audio.ToneLeftHz != 900 {
		t.Fatalf("unexpected left tone: %v", cfg.Audio.ToneLeftHz)
	}
}

func TestValidateRejectsBadFrequency(t *testing.T) {
	cfg := Default()
	cfg.FM.FrequencyMHz = 200
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected error")
	}
}

func TestValidateRejectsBadPreEmphasis(t *testing.T) {
	cfg := Default()
	cfg.FM.PreEmphasisTauUS = 150
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected error")
	}
}

func TestDefaultPreEmphasis(t *testing.T) {
	if Default().FM.PreEmphasisTauUS != 50 {
		t.Fatal("expected 50")
	}
}

func TestDefaultFMModulation(t *testing.T) {
	cfg := Default()
	if !cfg.FM.FMModulationEnabled {
		t.Fatal("expected true")
	}
	if cfg.FM.MaxDeviationHz != 75000 {
		t.Fatal("expected 75000")
	}
}

func TestParsePI(t *testing.T) {
	tests := []struct {
		in   string
		want uint16
		ok   bool
	}{
		{"1234", 0x1234, true}, {"0xBEEF", 0xBEEF, true}, {"0XCAFE", 0xCAFE, true},
		{" 0x2345 ", 0x2345, true}, {"", 0, false}, {"nope", 0, false},
	}
	for _, tt := range tests {
		got, err := ParsePI(tt.in)
		if tt.ok && err != nil {
			t.Fatalf("ParsePI(%q): %v", tt.in, err)
		}
		if !tt.ok && err == nil {
			t.Fatalf("ParsePI(%q): expected error", tt.in)
		}
		if tt.ok && got != tt.want {
			t.Fatalf("ParsePI(%q): got %x want %x", tt.in, got, tt.want)
		}
	}
}

func TestValidateRejectsInvalidPI(t *testing.T) {
	cfg := Default()
	cfg.RDS.PI = "nope"
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected error")
	}
}

func TestValidateRejectsEmptyPI(t *testing.T) {
	cfg := Default()
	cfg.RDS.PI = ""
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected error")
	}
}

func TestValidateRejectsLongPS(t *testing.T) {
	cfg := Default()
	cfg.RDS.PS = "TOO_LONG_PS"
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected error for PS longer than 8 characters")
	}
}

func TestValidateRejectsLongRadioText(t *testing.T) {
	cfg := Default()
	cfg.RDS.RadioText = strings.Repeat("x", 65)
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected error for RadioText longer than 64 characters")
	}
}

func TestEffectiveDeviceRate(t *testing.T) {
	cfg := Default()
	if cfg.EffectiveDeviceRate() != float64(cfg.FM.CompositeRateHz) {
		t.Fatal("expected composite rate")
	}
	cfg.Backend.DeviceSampleRateHz = 912000
	if cfg.EffectiveDeviceRate() != 912000 {
		t.Fatal("expected 912000")
	}
}

func TestValidateRejectsUnsupportedIngestKind(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Kind = "unsupported"
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected error")
	}
}

func TestValidateRejectsInvalidSRTConfig(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Kind = "srt"
	cfg.Ingest.SRT.URL = ""
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected srt url error")
	}

	cfg = Default()
	cfg.Ingest.Kind = "srt"
	cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000"
	cfg.Ingest.SRT.Mode = "invalid"
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected srt mode error")
	}

	cfg = Default()
	cfg.Ingest.Kind = "srt"
	cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000"
	cfg.Ingest.SRT.SampleRateHz = 0
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected srt sample rate error")
	}

	cfg = Default()
	cfg.Ingest.Kind = "srt"
	cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000"
	cfg.Ingest.SRT.Channels = 3
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected srt channels error")
	}
}

func TestValidateRejectsInvalidAES67Config(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Kind = "aes67"
	cfg.Ingest.AES67.MulticastGroup = ""
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected aes67 multicast group error")
	}

	cfg = Default()
	cfg.Ingest.Kind = "aes67"
	cfg.Ingest.AES67.MulticastGroup = "239.10.20.30"
	cfg.Ingest.AES67.Port = 5004
	cfg.Ingest.AES67.Encoding = "L16"
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected aes67 encoding error")
	}

	cfg = Default()
	cfg.Ingest.Kind = "aes67"
	cfg.Ingest.AES67.MulticastGroup = "239.10.20.30"
	cfg.Ingest.AES67.Port = 5004
	cfg.Ingest.AES67.SDP = "v=0"
	cfg.Ingest.AES67.SDPPath = "stream.sdp"
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected mutually exclusive sdp/sdpPath error")
	}
}

func TestValidateAcceptsAES67WithSDPOnly(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Kind = "aes67"
	cfg.Ingest.AES67.MulticastGroup = ""
	cfg.Ingest.AES67.SDP = "v=0\r\ns=demo\r\nc=IN IP4 239.10.20.30\r\nm=audio 5004 RTP/AVP 97\r\na=rtpmap:97 L24/48000/2\r\n"
	if err := cfg.Validate(); err != nil {
		t.Fatalf("expected aes67 with SDP to validate: %v", err)
	}
}

func TestValidateAcceptsAES67WithDiscoveryOnly(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Kind = "aes67"
	cfg.Ingest.AES67.MulticastGroup = ""
	cfg.Ingest.AES67.Port = 0
	cfg.Ingest.AES67.Discovery.StreamName = "AES67-MAIN"
	if err := cfg.Validate(); err != nil {
		t.Fatalf("expected aes67 discovery config to validate: %v", err)
	}
}

func TestValidateRejectsAES67DiscoveryWithoutStreamName(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Kind = "aes67"
	cfg.Ingest.AES67.MulticastGroup = ""
	cfg.Ingest.AES67.Port = 0
	cfg.Ingest.AES67.Discovery.Enabled = true
	cfg.Ingest.AES67.Discovery.StreamName = ""
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected discovery streamName validation error")
	}
}

func TestValidateRejectsAES67DiscoverySAPPortOutOfRange(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Kind = "aes67"
	cfg.Ingest.AES67.MulticastGroup = ""
	cfg.Ingest.AES67.Port = 0
	cfg.Ingest.AES67.Discovery.StreamName = "AES67-MAIN"
	cfg.Ingest.AES67.Discovery.SAPPort = 70000
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected discovery sapPort validation error")
	}
}

func TestValidateRejectsUnsupportedIngestPCMShape(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Stdin.SampleRateHz = 0
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected sampleRate error")
	}

	cfg = Default()
	cfg.Ingest.HTTPRaw.Channels = 6
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected channels error")
	}

	cfg = Default()
	cfg.Ingest.Stdin.Format = "f32le"
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected format error")
	}
}

func TestValidateRejectsUnsupportedIcecastDecoder(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Icecast.Decoder = "mystery"
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected decoder error")
	}
}

func TestValidateAcceptsIcecastDecoderFallbackAlias(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Icecast.Decoder = "fallback"
	if err := cfg.Validate(); err != nil {
		t.Fatalf("expected fallback alias to be accepted: %v", err)
	}
}

func TestValidateRejectsIcecastRadioTextMaxLenOutOfRange(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Icecast.RadioText.MaxLen = 65
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected maxLen error")
	}
}

func TestValidateRejectsReconnectWithMissingBackoff(t *testing.T) {
	cfg := Default()
	cfg.Ingest.Reconnect.Enabled = true
	cfg.Ingest.Reconnect.InitialBackoffMs = 0
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected reconnect backoff error")
	}
}

func TestValidateRejectsZeroMpxGain(t *testing.T) {
	cfg := Default()
	cfg.FM.MpxGain = 0
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected mpxGain error")
	}
}

func TestValidateRejectsInvalidStereoMode(t *testing.T) {
	cfg := Default()
	cfg.FM.StereoMode = "weird"
	if err := cfg.Validate(); err == nil {
		t.Fatal("expected stereoMode validation error")
	}
}

func TestValidateAcceptsStereoModes(t *testing.T) {
	for _, mode := range []string{"DSB", "SSB", "VSB"} {
		cfg := Default()
		cfg.FM.StereoMode = mode
		if err := cfg.Validate(); err != nil {
			t.Fatalf("mode %s should validate: %v", mode, err)
		}
	}
}
