package stream

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/conf/configtest"
	"github.com/navidrome/navidrome/core/auth"
	"github.com/navidrome/navidrome/core/ffmpeg"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/model/request"
	"github.com/navidrome/navidrome/tests"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

// withProbe pre-populates ProbeData on a MediaFile from its own fields,
// so ensureProbed short-circuits and tests don't need mock ffprobe results.
func withProbe(mf *model.MediaFile) *model.MediaFile {
	probe := ffmpeg.AudioProbeResult{
		Codec:      mf.AudioCodec(),
		BitRate:    mf.BitRate,
		SampleRate: mf.SampleRate,
		BitDepth:   mf.BitDepth,
		Channels:   mf.Channels,
	}
	data, _ := json.Marshal(probe)
	mf.ProbeData = string(data)
	return mf
}

var _ = Describe("Decider", func() {
	var (
		ds  *tests.MockDataStore
		ff  *tests.MockFFmpeg
		svc TranscodeDecider
		ctx context.Context
	)

	BeforeEach(func() {
		ctx = GinkgoT().Context()
		ds = &tests.MockDataStore{
			MockedProperty:    &tests.MockedPropertyRepo{},
			MockedTranscoding: &tests.MockTranscodingRepo{},
		}
		ff = tests.NewMockFFmpeg("")
		auth.Init(ds)
		svc = NewTranscodeDecider(ds, ff)
	})

	Describe("MakeDecision", func() {
		Context("Direct Play", func() {
			It("allows direct play when profile matches", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{ProtocolHTTP}, MaxAudioChannels: 2},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
				Expect(decision.CanTranscode).To(BeFalse())
				Expect(decision.TranscodeReasons).To(BeEmpty())
			})

			It("rejects direct play when container doesn't match", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"mp3"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.TranscodeReasons).To(ContainElement(And(
					ContainSubstring("container 'flac' not supported"),
					ContainSubstring("[mp3]"),
				)))
			})

			It("rejects direct play when codec doesn't match", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "m4a", Codec: "ALAC", BitRate: 1000, Channels: 2})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.TranscodeReasons).To(ContainElement(And(
					ContainSubstring("audio codec 'alac' not supported"),
					ContainSubstring("[m4a/aac]"),
				)))
			})

			It("rejects direct play when channels exceed limit", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}, MaxAudioChannels: 2},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.TranscodeReasons).To(ContainElement(And(
					ContainSubstring("audio channels 6 not supported"),
					ContainSubstring("[flac]"),
					ContainSubstring("(max 2)"),
				)))
			})

			It("accepts WAV source against a wav codec profile (pcm->wav bridge)", func() {
				// ffprobe normalizes PCM variants (pcm_s16le etc) to codec "pcm", but
				// browsers advertise WAV support as audioCodecs:["wav"] via audio/wav MIME.
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "wav", Codec: "pcm", BitRate: 1411, Channels: 2})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"wav"}, AudioCodecs: []string{"wav"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

			It("does not accept AIFF (pcm in non-wav container) against a wav codec profile", func() {
				// AIFF files also normalize to codec="pcm" but use container="aiff".
				// Without the container guard they would falsely match a codec-only
				// ["wav"] profile and be direct-played as if they were WAV.
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "aiff", Codec: "pcm", BitRate: 1411, Channels: 2})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{AudioCodecs: []string{"wav"}, Protocols: []string{ProtocolHTTP}},
					},
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.TranscodeReasons).To(ContainElement(ContainSubstring("audio codec 'pcm'")))
			})

			It("handles container aliases (aac -> m4a)", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"aac"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

			It("handles container aliases (mp4 -> m4a)", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"mp4"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

			It("handles container aliases (opus -> ogg)", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "opus", Codec: "Opus", BitRate: 165, Channels: 2, SampleRate: 48000})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"ogg"}, AudioCodecs: []string{"opus"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

			It("handles codec aliases (adts -> aac)", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"m4a"}, AudioCodecs: []string{"adts"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

			It("allows when protocol list is empty (any protocol)", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, AudioCodecs: []string{"flac"}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

			It("allows when both container and codec lists are empty (wildcard)", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 128, Channels: 2})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{}, AudioCodecs: []string{}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})
		})

		Context("MaxAudioBitrate constraint", func() {
			It("revokes direct play when bitrate exceeds maxAudioBitrate", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1500, Channels: 2})
				ci := &ClientInfo{
					MaxAudioBitrate: 500, // kbps
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
					},
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeReasons).To(ContainElement("audio bitrate not supported"))
			})
		})

		Context("Transcoding", func() {
			It("selects transcoding when direct play isn't possible", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 256, // kbps
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"mp3"}, Protocols: []string{ProtocolHTTP}},
					},
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP, MaxAudioChannels: 2},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("mp3"))
				Expect(decision.TargetBitrate).To(Equal(256)) // kbps
				Expect(decision.TranscodeReasons).To(ContainElement(And(
					ContainSubstring("container 'flac' not supported"),
					ContainSubstring("[mp3]"),
				)))
			})

			It("rejects lossy to lossless transcoding", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "flac", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeFalse())
			})

			It("uses default bitrate when client doesn't specify", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, BitDepth: 16})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetBitrate).To(Equal(160)) // mp3 default from mock transcoding repo
			})

			It("preserves lossy bitrate when under max", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "ogg", BitRate: 192, Channels: 2})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 256, // kbps
					TranscodingProfiles: []Profile{
						{Container: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetBitrate).To(Equal(192)) // source bitrate in kbps
			})

			It("rejects format with no transcoding command available", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "wav", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeFalse())
			})

			It("applies maxAudioBitrate as final cap on transcoded stream", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2})
				ci := &ClientInfo{
					MaxAudioBitrate: 96, // kbps
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetBitrate).To(Equal(96)) // capped by maxAudioBitrate
			})

			It("selects first valid transcoding profile in order", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 48000, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"mp3"}, Protocols: []string{ProtocolHTTP}},
					},
					TranscodingProfiles: []Profile{
						{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP, MaxAudioChannels: 2},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("opus"))
			})
		})

		Context("Lossless to lossless transcoding", func() {
			It("allows lossless to lossless when samplerate needs downsampling", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 176400, BitDepth: 1})
				ci := &ClientInfo{
					MaxAudioBitrate: 1000,
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
					},
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("mp3"))
			})

			It("sets IsLossless=true on transcoded stream when target is lossless", func() {
				// Transcoding to mp3 (lossy) should result in IsLossless=false.
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.IsLossless).To(BeFalse()) // mp3 is lossy
			})
		})

		Context("No compatible profile", func() {
			It("returns error when nothing matches", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6})
				ci := &ClientInfo{}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.CanTranscode).To(BeFalse())
				Expect(decision.ErrorReason).To(Equal("no compatible playback profile found"))
			})
		})

		Context("Codec limitations on direct play", func() {
			It("rejects direct play when codec limitation fails (required)", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 512, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{ProtocolHTTP}},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "mp3",
							Limitations: []Limitation{
								{Name: LimitationAudioBitrate, Comparison: ComparisonLessThanEqual, Values: []string{"320"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.TranscodeReasons).To(ContainElement("audio bitrate not supported"))
			})

			It("allows direct play when optional limitation fails", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 512, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{ProtocolHTTP}},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "mp3",
							Limitations: []Limitation{
								{Name: LimitationAudioBitrate, Comparison: ComparisonLessThanEqual, Values: []string{"320"}, Required: false},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

			It("handles Equals comparison with multiple values", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "flac",
							Limitations: []Limitation{
								{Name: LimitationAudioChannels, Comparison: ComparisonEquals, Values: []string{"1", "2"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

			It("rejects when Equals comparison doesn't match any value", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "flac",
							Limitations: []Limitation{
								{Name: LimitationAudioChannels, Comparison: ComparisonEquals, Values: []string{"1", "2"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
			})

			It("rejects direct play when audioProfile limitation fails (required)", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "aac",
							Limitations: []Limitation{
								{Name: LimitationAudioProfile, Comparison: ComparisonEquals, Values: []string{"LC"}, Required: true},
							},
						},
					},
				}
				// Source profile is empty (not yet populated from scanner), so Equals("LC") fails
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.TranscodeReasons).To(ContainElement("audio profile not supported"))
			})

			It("allows direct play when audioProfile limitation is optional", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "aac",
							Limitations: []Limitation{
								{Name: LimitationAudioProfile, Comparison: ComparisonEquals, Values: []string{"LC"}, Required: false},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

			It("rejects direct play due to samplerate limitation", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "flac",
							Limitations: []Limitation{
								{Name: LimitationAudioSamplerate, Comparison: ComparisonLessThanEqual, Values: []string{"48000"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.TranscodeReasons).To(ContainElement("audio samplerate not supported"))
			})
		})

		Context("Codec limitations on transcoded output", func() {
			It("applies bitrate limitation to transcoded stream", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 192, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					MaxAudioBitrate: 96, // force transcode
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "mp3",
							Limitations: []Limitation{
								{Name: LimitationAudioBitrate, Comparison: ComparisonLessThanEqual, Values: []string{"96"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.Bitrate).To(Equal(96))
			})

			It("applies channel limitation to transcoded stream", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 48000, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "mp3",
							Limitations: []Limitation{
								{Name: LimitationAudioChannels, Comparison: ComparisonLessThanEqual, Values: []string{"2"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.Channels).To(Equal(2))
			})

			It("applies samplerate limitation to transcoded stream", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "mp3",
							Limitations: []Limitation{
								{Name: LimitationAudioSamplerate, Comparison: ComparisonLessThanEqual, Values: []string{"48000"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
			})

			It("applies bitdepth limitation to transcoded stream", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "flac",
							Limitations: []Limitation{
								{Name: LimitationAudioBitdepth, Comparison: ComparisonLessThanEqual, Values: []string{"16"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.BitDepth).To(Equal(16))
				Expect(decision.TargetBitDepth).To(Equal(16))
			})

			It("preserves source bit depth when no limitation applies", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 24})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.BitDepth).To(Equal(24))
				Expect(decision.TargetBitDepth).To(Equal(24))
			})

			It("rejects transcoding profile when GreaterThanEqual cannot be satisfied", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "mp3",
							Limitations: []Limitation{
								{Name: LimitationAudioSamplerate, Comparison: ComparisonGreaterThanEqual, Values: []string{"96000"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeFalse())
			})
		})

		Context("DSD sample rate conversion", func() {
			It("converts DSD sample rate to PCM-equivalent in decision", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("mp3"))
				// DSD64 2822400 / 8 = 352800, capped by MP3 max of 48000
				Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
				Expect(decision.TargetSampleRate).To(Equal(48000))
				// DSD 1-bit → 24-bit PCM
				Expect(decision.TranscodeStream.BitDepth).To(Equal(24))
				Expect(decision.TargetBitDepth).To(Equal(24))
			})

			It("converts DSD sample rate for FLAC target without codec limit", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("flac"))
				// DSD64 2822400 / 8 = 352800, FLAC has no hard max
				Expect(decision.TranscodeStream.SampleRate).To(Equal(352800))
				Expect(decision.TargetSampleRate).To(Equal(352800))
				// DSD 1-bit → 24-bit PCM
				Expect(decision.TranscodeStream.BitDepth).To(Equal(24))
				Expect(decision.TargetBitDepth).To(Equal(24))
			})

			It("applies codec profile limit to DSD-converted FLAC sample rate", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "flac",
							Limitations: []Limitation{
								{Name: LimitationAudioSamplerate, Comparison: ComparisonLessThanEqual, Values: []string{"48000"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				// DSD64 2822400 / 8 = 352800, capped by codec profile limit of 48000
				Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
				Expect(decision.TargetSampleRate).To(Equal(48000))
				// DSD 1-bit → 24-bit PCM
				Expect(decision.TranscodeStream.BitDepth).To(Equal(24))
				Expect(decision.TargetBitDepth).To(Equal(24))
			})

			It("applies audioBitdepth limitation to DSD-converted bit depth", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "flac", AudioCodec: "flac", Protocol: ProtocolHTTP},
					},
					CodecProfiles: []CodecProfile{
						{
							Type: CodecProfileTypeAudio,
							Name: "flac",
							Limitations: []Limitation{
								{Name: LimitationAudioBitdepth, Comparison: ComparisonLessThanEqual, Values: []string{"16"}, Required: true},
							},
						},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				// DSD 1-bit → 24-bit PCM, then capped by codec profile limit to 16-bit
				Expect(decision.TranscodeStream.BitDepth).To(Equal(16))
				Expect(decision.TargetBitDepth).To(Equal(16))
			})
		})

		Context("Codec channel limits", func() {
			It("clamps 6-channel FLAC to 2 channels when transcoding to MP3", func() {
				// Regression test for #5336: ffmpeg's mp3 encoder rejects >2 channels.
				// The decider must clamp to the codec's hard limit even when no
				// transcoding profile MaxAudioChannels is configured.
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("mp3"))
				Expect(decision.TranscodeStream.Channels).To(Equal(2))
				Expect(decision.TargetChannels).To(Equal(2))
			})

			It("honors a stricter profile MaxAudioChannels over the codec clamp", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP, MaxAudioChannels: 1},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.Channels).To(Equal(1))
				Expect(decision.TargetChannels).To(Equal(1))
			})

			It("applies the codec clamp when the profile limit is looser", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP, MaxAudioChannels: 4},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.Channels).To(Equal(2))
				Expect(decision.TargetChannels).To(Equal(2))
			})

			It("passes channels through unchanged for codecs with no hard limit", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "m4a", AudioCodec: "aac", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("aac"))
				Expect(decision.TranscodeStream.Channels).To(Equal(6))
				Expect(decision.TargetChannels).To(Equal(6))
			})
		})

		Context("Probe-based lossless detection", func() {
			It("uses probe codec name for lossless detection", func() {
				// WavPack files: ffprobe reports codec as "wavpack", suffix is ".wv"
				mf := &model.MediaFile{ID: "1", Suffix: "wv", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}
				probe := ffmpeg.AudioProbeResult{
					Codec: "wavpack", BitRate: 1000, SampleRate: 44100, BitDepth: 16, Channels: 2,
				}
				data, _ := json.Marshal(probe)
				mf.ProbeData = string(data)

				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
					MaxTranscodingAudioBitrate: 256,
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.SourceStream.IsLossless).To(BeTrue())
				Expect(decision.SourceStream.Codec).To(Equal("wavpack"))
				// Lossless source transcoding to MP3 should use MaxTranscodingAudioBitrate
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.Bitrate).To(Equal(256))
			})

			It("detects lossy from probe codec name", func() {
				mf := &model.MediaFile{ID: "1", Suffix: "ogg", BitRate: 192, Channels: 2, SampleRate: 48000}
				probe := ffmpeg.AudioProbeResult{
					Codec: "vorbis", BitRate: 192, SampleRate: 48000, BitDepth: 0, Channels: 2,
				}
				data, _ := json.Marshal(probe)
				mf.ProbeData = string(data)

				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"ogg"}, AudioCodecs: []string{"vorbis"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.SourceStream.IsLossless).To(BeFalse())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})
		})

		Context("Opus fixed sample rate", func() {
			It("sets Opus output to 48000Hz regardless of input", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 128,
					TranscodingProfiles: []Profile{
						{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("opus"))
				// Opus always outputs 48000Hz
				Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
				Expect(decision.TargetSampleRate).To(Equal(48000))
			})

			It("sets Opus output to 48000Hz even for 96kHz input", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1500, Channels: 2, SampleRate: 96000, BitDepth: 24})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 128,
					TranscodingProfiles: []Profile{
						{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
			})
		})

		Context("Container vs format separation", func() {
			It("preserves mp4 container when falling back to aac format", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 256,
					TranscodingProfiles: []Profile{
						{Container: "mp4", AudioCodec: "aac", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				// TargetFormat is the internal format used for transcoding ("aac")
				Expect(decision.TargetFormat).To(Equal("aac"))
				// Container in the response preserves what the client asked ("mp4")
				Expect(decision.TranscodeStream.Container).To(Equal("mp4"))
				Expect(decision.TranscodeStream.Codec).To(Equal("aac"))
			})

			It("uses container as format when container matches transcoding config", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 256,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("mp3"))
				Expect(decision.TranscodeStream.Container).To(Equal("mp3"))
			})
		})

		Context("MP3 max sample rate", func() {
			It("caps sample rate at 48000 for MP3", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1500, Channels: 2, SampleRate: 96000, BitDepth: 24})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.SampleRate).To(Equal(48000))
			})

			It("preserves sample rate at 44100 for MP3", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TranscodeStream.SampleRate).To(Equal(44100))
			})
		})

		Context("AAC max sample rate", func() {
			It("caps sample rate at 96000 for AAC", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "dsf", Codec: "DSD", BitRate: 5644, Channels: 2, SampleRate: 2822400, BitDepth: 1})
				ci := &ClientInfo{
					MaxTranscodingAudioBitrate: 320,
					TranscodingProfiles: []Profile{
						{Container: "aac", AudioCodec: "aac", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				// DSD64 2822400 / 8 = 352800, capped by AAC max of 96000
				Expect(decision.TranscodeStream.SampleRate).To(Equal(96000))
			})
		})

		Context("Typed transcode reasons from multiple profiles", func() {
			It("collects reasons from each failed direct play profile", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "ogg", Codec: "Vorbis", BitRate: 128, Channels: 2, SampleRate: 48000})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
						{Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{ProtocolHTTP}},
						{Containers: []string{"m4a", "mp4"}, AudioCodecs: []string{"aac"}, Protocols: []string{ProtocolHTTP}},
					},
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.TranscodeReasons).To(HaveLen(3))
				Expect(decision.TranscodeReasons[0]).To(ContainSubstring("container 'ogg' not supported"))
				Expect(decision.TranscodeReasons[0]).To(ContainSubstring("[flac]"))
				Expect(decision.TranscodeReasons[1]).To(ContainSubstring("container 'ogg' not supported"))
				Expect(decision.TranscodeReasons[1]).To(ContainSubstring("[mp3/mp3]"))
				Expect(decision.TranscodeReasons[2]).To(ContainSubstring("container 'ogg' not supported"))
				Expect(decision.TranscodeReasons[2]).To(ContainSubstring("[m4a,mp4/aac]"))
			})
		})

		Context("Source stream details", func() {
			It("populates source stream correctly with kbps bitrate", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24, Duration: 300.5, Size: 50000000})
				ci := &ClientInfo{
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.SourceStream.Container).To(Equal("flac"))
				Expect(decision.SourceStream.Codec).To(Equal("flac"))
				Expect(decision.SourceStream.Bitrate).To(Equal(1000)) // kbps
				Expect(decision.SourceStream.SampleRate).To(Equal(96000))
				Expect(decision.SourceStream.BitDepth).To(Equal(24))
				Expect(decision.SourceStream.Channels).To(Equal(2))
			})
		})

		Context("Server-side player transcoding override", func() {
			It("forces transcoding when override targets a different format", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					Name: "TestClient",
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				// Set server override in context
				overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
				overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})

				decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("mp3"))
				Expect(decision.TargetBitrate).To(Equal(192))
			})

			It("allows direct play when source matches forced format and bitrate is within cap", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 128, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					Name: "TestClient",
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 256})

				decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
				Expect(decision.CanTranscode).To(BeFalse())
			})

			It("transcodes when source bitrate exceeds the forced cap", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					Name: "TestClient",
				}
				overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})

				decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("mp3"))
				Expect(decision.TargetBitrate).To(Equal(192))
			})

			It("uses player MaxBitRate over transcoding DefaultBitRate", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					Name: "TestClient",
				}
				overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 192})
				overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 320})

				decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("mp3"))
				Expect(decision.TargetBitrate).To(Equal(320))
			})

			It("applies no bitrate cap when both MaxBitRate and DefaultBitRate are 0", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					Name: "TestClient",
				}
				overrideCtx := request.WithTranscoding(ctx, model.Transcoding{TargetFormat: "mp3", DefaultBitRate: 0})
				overrideCtx = request.WithPlayer(overrideCtx, model.Player{MaxBitRate: 0})

				decision, err := svc.MakeDecision(overrideCtx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetFormat).To(Equal("mp3"))
				// With no cap, lossless→lossy uses format default bitrate (160 for mp3 from mock)
				Expect(decision.TargetBitrate).To(Equal(160))
			})

			It("does not apply override when no transcoding is in context", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					Name: "TestClient",
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				// No override in context — client profiles used as-is
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})

		})

		Context("Player MaxBitRate cap", func() {
			It("applies player MaxBitRate cap when client has no limit", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					Name: "TestClient",
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"flac", "mp3"}, AudioCodecs: []string{"flac", "mp3"}, Protocols: []string{ProtocolHTTP}},
					},
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 320})

				decision, err := svc.MakeDecision(playerCtx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				// Source bitrate 1000 > player cap 320, so direct play is not possible
				Expect(decision.CanDirectPlay).To(BeFalse())
				Expect(decision.CanTranscode).To(BeTrue())
				// Lossless→lossy should use MaxAudioBitrate (320) as target, not format default
				Expect(decision.TargetBitrate).To(Equal(320))
			})

			It("uses client limit when it is more restrictive than player MaxBitRate", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					Name:                       "TestClient",
					MaxAudioBitrate:            256,
					MaxTranscodingAudioBitrate: 256,
					TranscodingProfiles: []Profile{
						{Container: "mp3", AudioCodec: "mp3", Protocol: ProtocolHTTP},
					},
				}
				playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 500})

				decision, err := svc.MakeDecision(playerCtx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				// Client limit 256 < player cap 500, so player cap doesn't apply; client limit wins
				Expect(decision.TargetBitrate).To(Equal(256))
			})

			It("does not cap when player MaxBitRate is 0", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100})
				ci := &ClientInfo{
					Name: "TestClient",
					DirectPlayProfiles: []DirectPlayProfile{
						{Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{ProtocolHTTP}},
					},
				}
				playerCtx := request.WithPlayer(ctx, model.Player{MaxBitRate: 0})

				decision, err := svc.MakeDecision(playerCtx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanDirectPlay).To(BeTrue())
			})
		})

		Context("Format-aware default bitrate", func() {
			It("uses opus default bitrate from DB", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 48000, BitDepth: 16})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "opus", AudioCodec: "opus", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetBitrate).To(Equal(96)) // opus default from mock
			})

			It("uses aac default bitrate from DB", func() {
				mf := withProbe(&model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16})
				ci := &ClientInfo{
					TranscodingProfiles: []Profile{
						{Container: "aac", AudioCodec: "aac", Protocol: ProtocolHTTP},
					},
				}
				decision, err := svc.MakeDecision(ctx, mf, ci, TranscodeOptions{})
				Expect(err).ToNot(HaveOccurred())
				Expect(decision.CanTranscode).To(BeTrue())
				Expect(decision.TargetBitrate).To(Equal(256)) // aac default from mock
			})

			It("falls back to 256 for unknown format", func() {
				bitrate := lookupDefaultBitrate(ctx, ds, "xyz")
				Expect(bitrate).To(Equal(fallbackBitrate))
			})
		})

	})

	Describe("ensureProbed", func() {
		var mockMFRepo *tests.MockMediaFileRepo

		BeforeEach(func() {
			mockMFRepo = tests.CreateMockMediaFileRepo()
			ds.MockedMediaFile = mockMFRepo
		})

		It("calls ffprobe and populates ProbeData when empty", func() {
			mf := &model.MediaFile{ID: "probe-1", Suffix: "mp3", BitRate: 320, Channels: 2}
			mockMFRepo.SetData(model.MediaFiles{*mf})

			ff.ProbeAudioResult = &ffmpeg.AudioProbeResult{
				Codec: "mp3", BitRate: 320, SampleRate: 44100, Channels: 2,
			}

			svc := NewTranscodeDecider(ds, ff).(*deciderService)
			probe, err := svc.ensureProbed(ctx, mf)
			Expect(err).ToNot(HaveOccurred())
			Expect(mf.ProbeData).ToNot(BeEmpty())
			Expect(probe).ToNot(BeNil())
			Expect(probe.Codec).To(Equal("mp3"))
			Expect(probe.BitRate).To(Equal(320))
			Expect(probe.SampleRate).To(Equal(44100))
			Expect(probe.Channels).To(Equal(2))

			// Verify persisted to DB
			stored := mockMFRepo.Data["probe-1"]
			Expect(stored.ProbeData).To(Equal(mf.ProbeData))
		})

		It("skips ffprobe when ProbeData is already set", func() {
			mf := withProbe(&model.MediaFile{ID: "probe-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2})

			// Set error on mock — if ffprobe were called, this would fail
			ff.Error = fmt.Errorf("should not be called")

			svc := NewTranscodeDecider(ds, ff).(*deciderService)
			probe, err := svc.ensureProbed(ctx, mf)
			Expect(err).ToNot(HaveOccurred())
			Expect(probe).To(BeNil())
		})

		It("returns error when ffprobe fails", func() {
			mf := &model.MediaFile{ID: "probe-3", Suffix: "mp3"}
			ff.Error = fmt.Errorf("ffprobe not found")

			svc := NewTranscodeDecider(ds, ff).(*deciderService)
			_, err := svc.ensureProbed(ctx, mf)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("probing media file"))
			Expect(mf.ProbeData).To(BeEmpty())
		})

		It("skips ffprobe when DevEnableMediaFileProbe is false", func() {
			DeferCleanup(configtest.SetupConfig())
			conf.Server.DevEnableMediaFileProbe = false

			mf := &model.MediaFile{ID: "probe-4", Suffix: "mp3"}
			// Set a result — if ffprobe were called, ProbeData would be populated
			ff.ProbeAudioResult = &ffmpeg.AudioProbeResult{Codec: "mp3"}

			svc := NewTranscodeDecider(ds, ff).(*deciderService)
			probe, err := svc.ensureProbed(ctx, mf)
			Expect(err).ToNot(HaveOccurred())
			Expect(probe).To(BeNil())
			Expect(mf.ProbeData).To(BeEmpty())
		})
	})

})
