// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package rtc

import (
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/pion/webrtc/v4"
	"github.com/stretchr/testify/require"
	"github.com/twitchtv/twirp"
	"go.uber.org/atomic"
	"google.golang.org/protobuf/proto"

	"github.com/livekit/livekit-server/pkg/sfu/buffer"
	"github.com/livekit/protocol/auth"
	protoCodecs "github.com/livekit/protocol/codecs"
	"github.com/livekit/protocol/codecs/mime"
	"github.com/livekit/protocol/livekit"
	"github.com/livekit/protocol/logger"
	"github.com/livekit/protocol/observability/roomobs"
	lksdp "github.com/livekit/protocol/sdp"
	"github.com/livekit/protocol/signalling"
	"github.com/livekit/protocol/utils"
	"github.com/livekit/protocol/utils/guid"

	"github.com/livekit/livekit-server/pkg/config"
	"github.com/livekit/livekit-server/pkg/routing"
	"github.com/livekit/livekit-server/pkg/routing/routingfakes"
	"github.com/livekit/livekit-server/pkg/rtc/types"
	"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
	"github.com/livekit/livekit-server/pkg/testutils"
)

func TestIsReady(t *testing.T) {
	tests := []struct {
		state livekit.ParticipantInfo_State
		ready bool
	}{
		{
			state: livekit.ParticipantInfo_JOINING,
			ready: false,
		},
		{
			state: livekit.ParticipantInfo_JOINED,
			ready: true,
		},
		{
			state: livekit.ParticipantInfo_ACTIVE,
			ready: true,
		},
		{
			state: livekit.ParticipantInfo_DISCONNECTED,
			ready: false,
		},
	}

	for _, test := range tests {
		t.Run(test.state.String(), func(t *testing.T) {
			p := &ParticipantImpl{}
			p.state.Store(test.state)
			require.Equal(t, test.ready, p.IsReady())
		})
	}
}

func TestSupportsMoving(t *testing.T) {
	t.Run("current protocol version", func(t *testing.T) {
		p := newParticipantForTestWithOpts("test", &participantOpts{protocolVersion: types.CurrentProtocol})
		require.NoError(t, p.SupportsMoving())
	})

	// a move rejected because of what the participant is cannot be retried into working, but it is
	// still a caller error, so it has to surface as a 4xx rather than a 500
	t.Run("client version that cannot be moved", func(t *testing.T) {
		p := newParticipantForTestWithOpts("test", &participantOpts{protocolVersion: 6})

		err := p.SupportsMoving()
		require.ErrorIs(t, err, ErrMoveOldClientVersion)

		var twirpErr twirp.Error
		require.ErrorAs(t, err, &twirpErr)
		require.Equal(t, twirp.FailedPrecondition, twirpErr.Code())
	})
}

func TestTrackPublishing(t *testing.T) {
	t.Run("should send the correct events", func(t *testing.T) {
		p := newParticipantForTest("test")
		track := &typesfakes.FakeMediaTrack{}
		track.IDReturns("id")
		published := false
		updated := false
		p.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackUpdatedCalls(func(p types.Participant, track types.MediaTrack) {
			updated = true
		})
		p.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackPublishedCalls(func(p types.Participant, track types.MediaTrack) {
			published = true
		})
		p.UpTrackManager.AddPublishedTrack(track)
		p.handleTrackPublished(track, false, false)
		require.True(t, published)
		require.False(t, updated)
		require.Len(t, p.UpTrackManager.publishedTracks, 1)
	})

	t.Run("sends back trackPublished event", func(t *testing.T) {
		p := newParticipantForTest("test")
		sink := p.params.Sink.(*routingfakes.FakeMessageSink)
		p.AddTrack(&livekit.AddTrackRequest{
			Cid:    "cid",
			Name:   "webcam",
			Type:   livekit.TrackType_VIDEO,
			Width:  1024,
			Height: 768,
		})
		require.Equal(t, 1, sink.WriteMessageCallCount())
		res := sink.WriteMessageArgsForCall(0).(*livekit.SignalResponse)
		require.IsType(t, &livekit.SignalResponse_TrackPublished{}, res.Message)
		published := res.Message.(*livekit.SignalResponse_TrackPublished).TrackPublished
		require.Equal(t, "cid", published.Cid)
		require.Equal(t, "webcam", published.Track.Name)
		require.Equal(t, livekit.TrackType_VIDEO, published.Track.Type)
		require.Equal(t, uint32(1024), published.Track.Width)
		require.Equal(t, uint32(768), published.Track.Height)
	})

	t.Run("should not allow adding of duplicate tracks", func(t *testing.T) {
		p := newParticipantForTest("test")
		sink := p.params.Sink.(*routingfakes.FakeMessageSink)
		p.AddTrack(&livekit.AddTrackRequest{
			Cid:  "cid",
			Name: "webcam",
			Type: livekit.TrackType_VIDEO,
		})
		p.AddTrack(&livekit.AddTrackRequest{
			Cid:  "cid",
			Name: "duplicate",
			Type: livekit.TrackType_AUDIO,
		})

		// error response on duplicate adds a message
		require.Equal(t, 2, sink.WriteMessageCallCount())
	})

	t.Run("should queue adding of duplicate tracks if already published by client id in signalling", func(t *testing.T) {
		p := newParticipantForTest("test")
		sink := p.params.Sink.(*routingfakes.FakeMessageSink)

		track := &typesfakes.FakeLocalMediaTrack{}
		track.HasSignalCidCalls(func(s string) bool { return s == "cid" })
		track.ToProtoReturns(&livekit.TrackInfo{})
		// directly add to publishedTracks without lock - for testing purpose only
		p.UpTrackManager.publishedTracks["cid"] = track

		p.AddTrack(&livekit.AddTrackRequest{
			Cid:  "cid",
			Name: "webcam",
			Type: livekit.TrackType_VIDEO,
		})
		// `queued` `RequestResponse` should add a message
		require.Equal(t, 1, sink.WriteMessageCallCount())
		require.Equal(t, 1, len(p.pendingTracks["cid"].trackInfos))

		// add again - it should be added to the queue
		p.AddTrack(&livekit.AddTrackRequest{
			Cid:  "cid",
			Name: "webcam",
			Type: livekit.TrackType_VIDEO,
		})
		// `queued` `RequestResponse`s should have been sent for duplicate additions
		require.Equal(t, 2, sink.WriteMessageCallCount())
		require.Equal(t, 2, len(p.pendingTracks["cid"].trackInfos))

		// check SID is the same
		require.Equal(t, p.pendingTracks["cid"].trackInfos[0].Sid, p.pendingTracks["cid"].trackInfos[1].Sid)
	})

	t.Run("should queue adding of duplicate tracks if already published by client id in sdp", func(t *testing.T) {
		p := newParticipantForTest("test")
		sink := p.params.Sink.(*routingfakes.FakeMessageSink)

		track := &typesfakes.FakeLocalMediaTrack{}
		track.ToProtoReturns(&livekit.TrackInfo{})
		track.HasSdpCidCalls(func(s string) bool { return s == "cid" })
		// directly add to publishedTracks without lock - for testing purpose only
		p.UpTrackManager.publishedTracks["cid"] = track

		p.AddTrack(&livekit.AddTrackRequest{
			Cid:  "cid",
			Name: "webcam",
			Type: livekit.TrackType_VIDEO,
		})
		// `queued` `RequestResponse` should add a message
		require.Equal(t, 1, sink.WriteMessageCallCount())
		require.Equal(t, 1, len(p.pendingTracks["cid"].trackInfos))

		// add again - it should be added to the queue
		p.AddTrack(&livekit.AddTrackRequest{
			Cid:  "cid",
			Name: "webcam",
			Type: livekit.TrackType_VIDEO,
		})
		// `queued` `RequestResponse`s should have been sent for duplicate additions
		require.Equal(t, 2, sink.WriteMessageCallCount())
		require.Equal(t, 2, len(p.pendingTracks["cid"].trackInfos))

		// check SID is the same
		require.Equal(t, p.pendingTracks["cid"].trackInfos[0].Sid, p.pendingTracks["cid"].trackInfos[1].Sid)
	})

	t.Run("should not allow adding disallowed sources", func(t *testing.T) {
		p := newParticipantForTest("test")
		p.SetPermission(&livekit.ParticipantPermission{
			CanPublish: true,
			CanPublishSources: []livekit.TrackSource{
				livekit.TrackSource_CAMERA,
			},
		})
		sink := p.params.Sink.(*routingfakes.FakeMessageSink)
		p.AddTrack(&livekit.AddTrackRequest{
			Cid:    "cid",
			Name:   "webcam",
			Source: livekit.TrackSource_CAMERA,
			Type:   livekit.TrackType_VIDEO,
		})
		require.Equal(t, 1, sink.WriteMessageCallCount())

		p.AddTrack(&livekit.AddTrackRequest{
			Cid:    "cid2",
			Name:   "rejected source",
			Type:   livekit.TrackType_AUDIO,
			Source: livekit.TrackSource_MICROPHONE,
		})
		// an error response for disallowed source should send a `RequestResponse`.
		require.Equal(t, 2, sink.WriteMessageCallCount())
	})
}

func TestOutOfOrderUpdates(t *testing.T) {
	p := newParticipantForTest("test")
	p.updateState(livekit.ParticipantInfo_JOINED)
	p.SetMetadata("initial metadata")
	sink := p.GetResponseSink().(*routingfakes.FakeMessageSink)
	pi1 := p.ToProto()
	p.SetMetadata("second update")
	pi2 := p.ToProto()

	require.Greater(t, pi2.Version, pi1.Version)

	// send the second update first
	require.NoError(t, p.SendParticipantUpdate([]*livekit.ParticipantInfo{pi2}))
	require.NoError(t, p.SendParticipantUpdate([]*livekit.ParticipantInfo{pi1}))

	// only sent once, and it's the earlier message
	require.Equal(t, 1, sink.WriteMessageCallCount())
	sent := sink.WriteMessageArgsForCall(0).(*livekit.SignalResponse)
	require.Equal(t, "second update", sent.GetUpdate().Participants[0].Metadata)
}

// after disconnection, things should continue to function and not panic
func TestDisconnectTiming(t *testing.T) {
	t.Run("Negotiate doesn't panic after channel closed", func(t *testing.T) {
		p := newParticipantForTest("test")
		msg := routing.NewMessageChannel(livekit.ConnectionID("test"), routing.DefaultMessageChannelSize)
		p.params.Sink = msg
		go func() {
			for msg := range msg.ReadChan() {
				t.Log("received message from chan", msg)
			}
		}()
		track := &typesfakes.FakeMediaTrack{}
		p.UpTrackManager.AddPublishedTrack(track)
		p.handleTrackPublished(track, false, false)

		// close channel and then try to Negotiate
		msg.Close()
	})
}

func TestCorrectJoinedAt(t *testing.T) {
	p := newParticipantForTest("test")
	info := p.ToProto()
	require.NotZero(t, info.JoinedAt)
	require.True(t, time.Now().Unix()-info.JoinedAt <= 1)
}

func TestMuteSetting(t *testing.T) {
	t.Run("can set mute when track is pending", func(t *testing.T) {
		p := newParticipantForTest("test")
		ti := &livekit.TrackInfo{Sid: "testTrack"}
		p.pendingTracks["cid"] = &pendingTrackInfo{trackInfos: []*livekit.TrackInfo{ti}}

		p.SetTrackMuted(&livekit.MuteTrackRequest{
			Sid:   ti.Sid,
			Muted: true,
		}, false)
		require.True(t, p.pendingTracks["cid"].trackInfos[0].Muted)
	})

	t.Run("can publish a muted track", func(t *testing.T) {
		p := newParticipantForTest("test")
		p.AddTrack(&livekit.AddTrackRequest{
			Cid:   "cid",
			Type:  livekit.TrackType_AUDIO,
			Muted: true,
		})

		_, ti, _, _, _ := p.getPendingTrack("cid", livekit.TrackType_AUDIO, false)
		require.NotNil(t, ti)
		require.True(t, ti.Muted)
	})
}

func TestSubscriberAsPrimary(t *testing.T) {
	t.Run("protocol 4 uses subs as primary", func(t *testing.T) {
		p := newParticipantForTestWithOpts("test", &participantOpts{
			permissions: &livekit.ParticipantPermission{
				CanSubscribe: true,
				CanPublish:   true,
			},
		})
		require.True(t, p.SubscriberAsPrimary())
	})

	t.Run("protocol 2 uses pub as primary", func(t *testing.T) {
		p := newParticipantForTestWithOpts("test", &participantOpts{
			protocolVersion: 2,
			permissions: &livekit.ParticipantPermission{
				CanSubscribe: true,
				CanPublish:   true,
			},
		})
		require.False(t, p.SubscriberAsPrimary())
	})

	t.Run("publisher only uses pub as primary", func(t *testing.T) {
		p := newParticipantForTestWithOpts("test", &participantOpts{
			permissions: &livekit.ParticipantPermission{
				CanSubscribe: false,
				CanPublish:   true,
			},
		})
		require.False(t, p.SubscriberAsPrimary())

		// ensure that it doesn't change after perms
		p.SetPermission(&livekit.ParticipantPermission{
			CanSubscribe: true,
			CanPublish:   true,
		})
		require.False(t, p.SubscriberAsPrimary())
	})
}

func TestDisableCodecs(t *testing.T) {
	participant := newParticipantForTestWithOpts("123", &participantOpts{
		publisher: false,
		clientConf: &livekit.ClientConfiguration{
			DisabledCodecs: &livekit.DisabledCodecs{
				Codecs: []*livekit.Codec{
					{Mime: "video/h264"},
				},
			},
		},
	})

	participant.SetMigrateState(types.MigrateStateComplete)

	pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
	require.NoError(t, err)
	transceiver, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
	require.NoError(t, err)
	sdp, err := pc.CreateOffer(nil)
	require.NoError(t, err)
	pc.SetLocalDescription(sdp)
	codecs := transceiver.Receiver().GetParameters().Codecs
	var found264 bool
	for _, c := range codecs {
		if mime.IsMimeTypeStringH264(c.MimeType) {
			found264 = true
		}
	}
	require.True(t, found264)
	offerId := uint32(42)

	// negotiated codec should not contain h264
	sink := &routingfakes.FakeMessageSink{}
	participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
	var answer webrtc.SessionDescription
	var answerId uint32
	var answerReceived atomic.Bool
	var answerIdReceived atomic.Uint32
	sink.WriteMessageCalls(func(msg proto.Message) error {
		if res, ok := msg.(*livekit.SignalResponse); ok {
			if res.GetAnswer() != nil {
				answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
				answerReceived.Store(true)
				answerIdReceived.Store(answerId)
			}
		}
		return nil
	})
	participant.HandleOffer(&livekit.SessionDescription{
		Type: webrtc.SDPTypeOffer.String(),
		Sdp:  sdp.SDP,
		Id:   offerId,
	})

	testutils.WithTimeout(t, func() string {
		if answerReceived.Load() && answerIdReceived.Load() == offerId {
			return ""
		} else {
			return "answer not received OR answer id mismatch"
		}
	})
	require.NoError(t, pc.SetRemoteDescription(answer), answer.SDP, sdp.SDP)

	codecs = transceiver.Receiver().GetParameters().Codecs
	found264 = false
	for _, c := range codecs {
		if mime.IsMimeTypeStringH264(c.MimeType) {
			found264 = true
		}
	}
	require.False(t, found264)
}

func TestDisablePublishCodec(t *testing.T) {
	participant := newParticipantForTestWithOpts("123", &participantOpts{
		publisher: true,
		clientConf: &livekit.ClientConfiguration{
			DisabledCodecs: &livekit.DisabledCodecs{
				Publish: []*livekit.Codec{
					{Mime: "video/h264"},
				},
			},
		},
	})

	for _, codec := range participant.enabledPublishCodecs {
		require.False(t, mime.IsMimeTypeStringH264(codec.Mime))
	}

	sink := &routingfakes.FakeMessageSink{}
	participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
	var publishReceived atomic.Bool
	sink.WriteMessageCalls(func(msg proto.Message) error {
		if res, ok := msg.(*livekit.SignalResponse); ok {
			if published := res.GetTrackPublished(); published != nil {
				publishReceived.Store(true)
				require.NotEmpty(t, published.Track.Codecs)
				require.True(t, mime.IsMimeTypeStringVP8(published.Track.Codecs[0].MimeType))
			}
		}
		return nil
	})

	// simulcast codec response should pick an alternative
	participant.AddTrack(&livekit.AddTrackRequest{
		Cid:  "cid1",
		Type: livekit.TrackType_VIDEO,
		SimulcastCodecs: []*livekit.SimulcastCodec{{
			Codec: "h264",
			Cid:   "cid1",
		}},
	})

	require.Eventually(t, func() bool { return publishReceived.Load() }, 5*time.Second, 10*time.Millisecond)

	// publishing a supported codec should not change
	publishReceived.Store(false)
	sink.WriteMessageCalls(func(msg proto.Message) error {
		if res, ok := msg.(*livekit.SignalResponse); ok {
			if published := res.GetTrackPublished(); published != nil {
				publishReceived.Store(true)
				require.NotEmpty(t, published.Track.Codecs)
				require.True(t, mime.IsMimeTypeStringVP8(published.Track.Codecs[0].MimeType))
			}
		}
		return nil
	})
	participant.AddTrack(&livekit.AddTrackRequest{
		Cid:  "cid2",
		Type: livekit.TrackType_VIDEO,
		SimulcastCodecs: []*livekit.SimulcastCodec{{
			Codec: "vp8",
			Cid:   "cid2",
		}},
	})
	require.Eventually(t, func() bool { return publishReceived.Load() }, 5*time.Second, 10*time.Millisecond)
}

func TestPreferMediaCodecForPublisher(t *testing.T) {
	testCases := []struct {
		name                       string
		mediaKind                  string
		trackBaseCid               string
		preferredCodec             string
		addTrack                   *livekit.AddTrackRequest
		mimeTypeStringChecker      func(string) bool
		mimeTypeCodecStringChecker func(string) bool
		transceiverMimeType        mime.MimeType
	}{
		{
			name:           "video",
			mediaKind:      "video",
			trackBaseCid:   "preferH264Video",
			preferredCodec: "h264",
			addTrack: &livekit.AddTrackRequest{
				Type:   livekit.TrackType_VIDEO,
				Name:   "video",
				Width:  1280,
				Height: 720,
				Source: livekit.TrackSource_CAMERA,
			},
			mimeTypeStringChecker:      mime.IsMimeTypeStringH264,
			mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringH264,
			transceiverMimeType:        mime.MimeTypeVP8,
		},
		{
			name:           "audio",
			mediaKind:      "audio",
			trackBaseCid:   "preferPCMAAudio",
			preferredCodec: "pcma",
			addTrack: &livekit.AddTrackRequest{
				Type:   livekit.TrackType_AUDIO,
				Name:   "audio",
				Source: livekit.TrackSource_MICROPHONE,
			},
			mimeTypeStringChecker:      mime.IsMimeTypeStringPCMA,
			mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringPCMA,
			transceiverMimeType:        mime.MimeTypeOpus,
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			participant := newParticipantForTestWithOpts("123", &participantOpts{
				publisher: true,
			})
			participant.SetMigrateState(types.MigrateStateComplete)

			pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
			require.NoError(t, err)
			defer pc.Close()

			for i := 0; i < 2; i++ {
				// publish preferred track without client using setCodecPreferences()
				trackCid := fmt.Sprintf("%s-%d", tc.trackBaseCid, i)
				req := utils.CloneProto(tc.addTrack)
				req.SimulcastCodecs = []*livekit.SimulcastCodec{
					{
						Codec: tc.preferredCodec,
						Cid:   trackCid,
					},
				}
				participant.AddTrack(req)

				track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: tc.transceiverMimeType.String()}, trackCid, trackCid)
				require.NoError(t, err)
				transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
				require.NoError(t, err)
				codecs := transceiver.Receiver().GetParameters().Codecs

				if i > 0 {
					// the negotiated codecs order could be updated by first negotiation,
					// reorder to make tested preferred codec not preferred
					for tc.mimeTypeStringChecker(codecs[0].MimeType) {
						codecs = append(codecs[1:], codecs[0])
					}
				}
				// preferred codec should not be preferred in `offer`
				require.False(t, tc.mimeTypeStringChecker(codecs[0].MimeType), "codecs", codecs)

				sdp, err := pc.CreateOffer(nil)
				require.NoError(t, err)
				require.NoError(t, pc.SetLocalDescription(sdp))
				offerId := uint32(23)

				sink := &routingfakes.FakeMessageSink{}
				participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
				var answer webrtc.SessionDescription
				var answerId uint32
				var answerReceived atomic.Bool
				var answerIdReceived atomic.Uint32
				sink.WriteMessageCalls(func(msg proto.Message) error {
					if res, ok := msg.(*livekit.SignalResponse); ok {
						if res.GetAnswer() != nil {
							answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
							pc.SetRemoteDescription(answer)
							answerReceived.Store(true)
							answerIdReceived.Store(answerId)
						}
					}
					return nil
				})
				participant.HandleOffer(&livekit.SessionDescription{
					Type: webrtc.SDPTypeOffer.String(),
					Sdp:  sdp.SDP,
					Id:   offerId,
				})

				require.Eventually(t, func() bool { return answerReceived.Load() && answerIdReceived.Load() == offerId }, 5*time.Second, 10*time.Millisecond)

				var havePreferred bool
				parsed, err := answer.Unmarshal()
				require.NoError(t, err)
				var mediaSectionIndex int
				for _, m := range parsed.MediaDescriptions {
					if m.MediaName.Media == tc.mediaKind {
						if mediaSectionIndex == i {
							codecs, err := lksdp.CodecsFromMediaDescription(m)
							require.NoError(t, err)
							if tc.mimeTypeCodecStringChecker(codecs[0].Name) {
								havePreferred = true
								break
							}
						}
						mediaSectionIndex++
					}
				}

				require.Truef(t, havePreferred, "%s should be preferred for %s section %d, answer sdp: \n%s", tc.preferredCodec, tc.mediaKind, i, answer.SDP)
			}
		})
	}
}

func TestPreferAudioCodecForRed(t *testing.T) {
	participant := newParticipantForTestWithOpts("123", &participantOpts{
		publisher: true,
	})
	participant.SetMigrateState(types.MigrateStateComplete)

	me := webrtc.MediaEngine{}
	opusCodecParameters := protoCodecs.OpusCodecParameters
	opusCodecParameters.RTPCodecCapability.RTCPFeedback = []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}
	require.NoError(t, me.RegisterCodec(opusCodecParameters, webrtc.RTPCodecTypeAudio))
	redCodecParameters := protoCodecs.RedCodecParameters
	redCodecParameters.RTPCodecCapability.RTCPFeedback = []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}
	require.NoError(t, me.RegisterCodec(redCodecParameters, webrtc.RTPCodecTypeAudio))

	api := webrtc.NewAPI(webrtc.WithMediaEngine(&me))
	pc, err := api.NewPeerConnection(webrtc.Configuration{})
	require.NoError(t, err)
	defer pc.Close()

	for idx, disableRed := range []bool{false, true, false, true} {
		t.Run(fmt.Sprintf("disableRed=%v", disableRed), func(t *testing.T) {
			trackCid := fmt.Sprintf("audiotrack%d", idx)
			req := &livekit.AddTrackRequest{
				Type: livekit.TrackType_AUDIO,
				Cid:  trackCid,
			}
			if idx < 2 {
				req.DisableRed = disableRed
			} else {
				codec := "red"
				if disableRed {
					codec = "opus"
				}
				req.SimulcastCodecs = []*livekit.SimulcastCodec{
					{
						Codec: codec,
						Cid:   trackCid,
					},
				}
			}
			participant.AddTrack(req)

			track, err := webrtc.NewTrackLocalStaticRTP(
				webrtc.RTPCodecCapability{MimeType: "audio/opus"},
				trackCid,
				trackCid,
			)
			require.NoError(t, err)

			transceiver, err := pc.AddTransceiverFromTrack(
				track,
				webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv},
			)
			require.NoError(t, err)
			codecs := transceiver.Sender().GetParameters().Codecs
			for i, c := range codecs {
				if c.MimeType == "audio/opus" {
					if i != 0 {
						codecs[0], codecs[i] = codecs[i], codecs[0]
					}
					break
				}
			}
			transceiver.SetCodecPreferences(codecs)
			sdp, err := pc.CreateOffer(nil)
			require.NoError(t, err)
			pc.SetLocalDescription(sdp)
			// opus should be preferred
			require.Equal(t, codecs[0].MimeType, "audio/opus", sdp)
			offerId := uint32(0xffffff)

			sink := &routingfakes.FakeMessageSink{}
			participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
			var answer webrtc.SessionDescription
			var answerId uint32
			var answerReceived atomic.Bool
			var answerIdReceived atomic.Uint32
			sink.WriteMessageCalls(func(msg proto.Message) error {
				if res, ok := msg.(*livekit.SignalResponse); ok {
					if res.GetAnswer() != nil {
						answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
						pc.SetRemoteDescription(answer)
						answerReceived.Store(true)
						answerIdReceived.Store(answerId)
					}
				}
				return nil
			})
			participant.HandleOffer(&livekit.SessionDescription{
				Type: webrtc.SDPTypeOffer.String(),
				Sdp:  sdp.SDP,
				Id:   offerId,
			})
			require.Eventually(
				t,
				func() bool {
					return answerReceived.Load() && answerIdReceived.Load() == offerId
				},
				5*time.Second,
				10*time.Millisecond,
			)

			var redPreferred bool
			parsed, err := answer.Unmarshal()
			require.NoError(t, err)
			var audioSectionIndex int
			for _, m := range parsed.MediaDescriptions {
				if m.MediaName.Media == "audio" {
					if audioSectionIndex == idx {
						codecs, err := lksdp.CodecsFromMediaDescription(m)
						require.NoError(t, err)
						// nack is always enabled. if red is preferred, server will not generate nack request
						var nackEnabled bool
						for _, c := range codecs {
							if c.Name == "opus" {
								for _, fb := range c.RTCPFeedback {
									if strings.Contains(fb, "nack") {
										nackEnabled = true
										break
									}
								}
							}
						}
						require.True(t, nackEnabled, "nack should be enabled for opus")

						if mime.IsMimeTypeCodecStringRED(codecs[0].Name) {
							redPreferred = true
							break
						}
					}
					audioSectionIndex++
				}
			}
			require.Equalf(t, !disableRed, redPreferred, "offer : \n%s\nanswer sdp: \n%s", sdp, answer.SDP)
		})
	}
}

type participantOpts struct {
	permissions     *livekit.ParticipantPermission
	protocolVersion types.ProtocolVersion
	publisher       bool
	clientConf      *livekit.ClientConfiguration
	clientInfo      *livekit.ClientInfo
}

func newParticipantForTestWithOpts(identity livekit.ParticipantIdentity, opts *participantOpts) *ParticipantImpl {
	if opts == nil {
		opts = &participantOpts{}
	}
	if opts.protocolVersion == 0 {
		opts.protocolVersion = 6
	}
	conf, _ := config.NewConfig("", true, nil, nil)
	// disable mux, it doesn't play too well with unit test
	conf.RTC.TCPPort = 0
	rtcConf, err := NewWebRTCConfig(conf)
	if err != nil {
		panic(err)
	}
	ff := buffer.NewFactoryOfBufferFactory(500, 200)
	rtcConf.SetBufferFactory(ff.CreateBufferFactory())
	grants := &auth.ClaimGrants{
		Video: &auth.VideoGrant{},
	}
	if opts.permissions != nil {
		grants.Video.SetCanPublish(opts.permissions.CanPublish)
		grants.Video.SetCanPublishData(opts.permissions.CanPublishData)
		grants.Video.SetCanSubscribe(opts.permissions.CanSubscribe)
	}

	enabledCodecs := make([]*livekit.Codec, 0, len(conf.Room.EnabledCodecs))
	for _, c := range conf.Room.EnabledCodecs {
		enabledCodecs = append(enabledCodecs, &livekit.Codec{
			Mime:     c.Mime,
			FmtpLine: c.FmtpLine,
		})
	}
	sid := livekit.ParticipantID(guid.New(utils.ParticipantPrefix))
	p, _ := NewParticipant(ParticipantParams{
		SID:                    sid,
		Identity:               identity,
		Config:                 rtcConf,
		Sink:                   &routingfakes.FakeMessageSink{},
		ProtocolVersion:        opts.protocolVersion,
		SessionStartTime:       time.Now(),
		PLIThrottleConfig:      conf.RTC.PLIThrottle,
		Grants:                 grants,
		PublishEnabledCodecs:   enabledCodecs,
		SubscribeEnabledCodecs: enabledCodecs,
		ClientConf:             opts.clientConf,
		ClientInfo:             ClientInfo{ClientInfo: opts.clientInfo},
		Logger:                 LoggerWithParticipant(logger.GetLogger(), identity, sid, false),
		Reporter:               roomobs.NewNoopParticipantSessionReporter(),
		TelemetryListener:      &typesfakes.FakeParticipantTelemetryListener{},
		VersionGenerator:       utils.NewDefaultTimedVersionGenerator(),
		ParticipantListener:    &typesfakes.FakeLocalParticipantListener{},
		ParticipantHelper:      &typesfakes.FakeLocalParticipantHelper{},
	})
	p.isPublisher.Store(opts.publisher)
	p.updateState(livekit.ParticipantInfo_ACTIVE)

	return p
}

func newParticipantForTest(identity livekit.ParticipantIdentity) *ParticipantImpl {
	return newParticipantForTestWithOpts(identity, nil)
}

func TestUnsequencedReliableDataBufferedWhileJoining(t *testing.T) {
	// unsequenced reliable data, i. e. room service SendData, arriving before the
	// reliable data channel is writable should be held and replayed, not dropped
	t.Run("buffers and replays", func(t *testing.T) {
		p := newParticipantForTest("test")
		require.False(t, p.reliableDataInfo.canWriteReliable)

		require.NoError(t, p.SendDataMessage(livekit.DataPacket_RELIABLE, []byte("one"), "", 0))
		require.NoError(t, p.SendDataMessage(livekit.DataPacket_RELIABLE, []byte("two"), "", 0))

		require.Equal(t, [][]byte{[]byte("one"), []byte("two")}, p.reliableDataInfo.joiningUnsequencedMessages)
		require.Equal(t, 6, p.reliableDataInfo.joiningUnsequencedBytes)

		p.replayJoiningReliableMessages()

		require.True(t, p.reliableDataInfo.canWriteReliable)
		require.Empty(t, p.reliableDataInfo.joiningUnsequencedMessages)
		require.Zero(t, p.reliableDataInfo.joiningUnsequencedBytes)
	})

	t.Run("does not buffer once writable", func(t *testing.T) {
		p := newParticipantForTest("test")
		p.replayJoiningReliableMessages()

		// no data channel in test, so the write through fails rather than getting buffered
		require.Error(t, p.SendDataMessage(livekit.DataPacket_RELIABLE, []byte("one"), "", 0))
		require.Empty(t, p.reliableDataInfo.joiningUnsequencedMessages)
	})

	t.Run("bounded buffer", func(t *testing.T) {
		p := newParticipantForTest("test")

		data := make([]byte, cMaxJoiningUnsequencedReliableBytes)
		require.NoError(t, p.SendDataMessage(livekit.DataPacket_RELIABLE, data, "", 0))
		require.Error(t, p.SendDataMessage(livekit.DataPacket_RELIABLE, []byte("overflow"), "", 0))

		require.Len(t, p.reliableDataInfo.joiningUnsequencedMessages, 1)
		require.Equal(t, 1, p.reliableDataInfo.joiningUnsequencedDropped)
	})

	t.Run("lossy is not buffered", func(t *testing.T) {
		p := newParticipantForTest("test")

		require.Error(t, p.SendDataMessage(livekit.DataPacket_LOSSY, []byte("one"), "", 0))
		require.Empty(t, p.reliableDataInfo.joiningUnsequencedMessages)
	})
}
