// 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 sfu

import (
	"testing"

	"github.com/pion/rtp/codecs"
	"github.com/pion/webrtc/v4"
	"github.com/stretchr/testify/require"

	"github.com/livekit/mediatransportutil/pkg/codec"
	"github.com/livekit/protocol/codecs/mime"
	"github.com/livekit/protocol/livekit"
	"github.com/livekit/protocol/logger"

	"github.com/livekit/livekit-server/pkg/sfu/buffer"
	dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor"
	"github.com/livekit/livekit-server/pkg/sfu/testutils"
)

func disable(f *Forwarder) {
	f.vls.SetCurrent(buffer.InvalidLayer)
	f.vls.SetTarget(buffer.InvalidLayer)
}

func newForwarder(codec webrtc.RTPCodecCapability, kind webrtc.RTPCodecType) *Forwarder {
	f := NewForwarder(
		kind,
		logger.GetLogger(),
		true, // skipReferenceTS
		true, // disableOpportunisticAllocation
		true, // enableStartAtDesiredQuality
		nil,
	)
	f.DetermineCodec(codec, nil, livekit.VideoLayer_MODE_UNUSED)
	return f
}

func TestForwarderMute(t *testing.T) {
	f := newForwarder(testutils.TestOpusCodec, webrtc.RTPCodecTypeAudio)
	require.False(t, f.IsMuted())
	muted := f.Mute(false, true)
	require.False(t, muted) // no change in mute state
	require.False(t, f.IsMuted())

	muted = f.Mute(true, false)
	require.False(t, muted)
	require.False(t, f.IsMuted())

	muted = f.Mute(true, true)
	require.True(t, muted)
	require.True(t, f.IsMuted())

	muted = f.Mute(false, true)
	require.True(t, muted)
	require.False(t, f.IsMuted())
}

func TestForwarderLayersAudio(t *testing.T) {
	f := newForwarder(testutils.TestOpusCodec, webrtc.RTPCodecTypeAudio)

	require.Equal(t, buffer.InvalidLayer, f.MaxLayer())

	require.Equal(t, buffer.InvalidLayer, f.CurrentLayer())
	require.Equal(t, buffer.InvalidLayer, f.TargetLayer())

	changed, maxLayer := f.SetMaxSpatialLayer(1)
	require.False(t, changed)
	require.Equal(t, buffer.InvalidLayer, maxLayer)

	changed, maxLayer = f.SetMaxTemporalLayer(1)
	require.False(t, changed)
	require.Equal(t, buffer.InvalidLayer, maxLayer)

	require.Equal(t, buffer.InvalidLayer, f.MaxLayer())
}

func TestForwarderLayersVideo(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)

	maxLayer := f.MaxLayer()
	expectedLayers := buffer.VideoLayer{Spatial: buffer.InvalidLayerSpatial, Temporal: buffer.DefaultMaxLayerTemporal}
	require.Equal(t, expectedLayers, maxLayer)

	require.Equal(t, buffer.InvalidLayer, f.CurrentLayer())
	require.Equal(t, buffer.InvalidLayer, f.TargetLayer())

	expectedLayers = buffer.VideoLayer{
		Spatial:  buffer.DefaultMaxLayerSpatial,
		Temporal: buffer.DefaultMaxLayerTemporal,
	}
	changed, maxLayer := f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	require.True(t, changed)
	require.Equal(t, expectedLayers, maxLayer)

	changed, maxLayer = f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial - 1)
	require.True(t, changed)
	expectedLayers = buffer.VideoLayer{
		Spatial:  buffer.DefaultMaxLayerSpatial - 1,
		Temporal: buffer.DefaultMaxLayerTemporal,
	}
	require.Equal(t, expectedLayers, maxLayer)
	require.Equal(t, expectedLayers, f.MaxLayer())

	f.vls.SetCurrent(buffer.VideoLayer{Spatial: 0, Temporal: 1})
	changed, maxLayer = f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial - 1)
	require.False(t, changed)
	require.Equal(t, expectedLayers, maxLayer)
	require.Equal(t, expectedLayers, f.MaxLayer())

	changed, maxLayer = f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)
	require.False(t, changed)
	require.Equal(t, expectedLayers, maxLayer)

	changed, maxLayer = f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal - 1)
	require.True(t, changed)
	expectedLayers = buffer.VideoLayer{
		Spatial:  buffer.DefaultMaxLayerSpatial - 1,
		Temporal: buffer.DefaultMaxLayerTemporal - 1,
	}
	require.Equal(t, expectedLayers, maxLayer)
	require.Equal(t, expectedLayers, f.MaxLayer())
}

func TestForwarderAllocateOptimal(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)

	emptyBitrates := Bitrates{}
	bitrates := Bitrates{
		{2, 3, 0, 0},
		{4, 0, 0, 5},
		{0, 7, 0, 0},
	}

	// invalid max layers
	f.vls.SetMax(buffer.InvalidLayer)
	expectedResult := VideoAllocation{
		PauseReason:         VideoPauseReasonFeedDry,
		BandwidthRequested:  0,
		BandwidthDelta:      0,
		Bitrates:            bitrates,
		TargetLayer:         buffer.InvalidLayer,
		RequestLayerSpatial: buffer.InvalidLayerSpatial,
		MaxLayer:            buffer.InvalidLayer,
		DistanceToDesired:   0,
	}
	result := f.AllocateOptimal(nil, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)

	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)

	// should still have target at buffer.InvalidLayer until max publisher layer is available
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonFeedDry,
		BandwidthRequested:  0,
		BandwidthDelta:      0,
		Bitrates:            bitrates,
		TargetLayer:         buffer.InvalidLayer,
		RequestLayerSpatial: buffer.InvalidLayerSpatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   0,
	}
	result = f.AllocateOptimal(nil, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)

	f.SetMaxPublishedLayer(buffer.DefaultMaxLayerSpatial)

	// muted should not consume any bandwidth
	f.Mute(true, true)
	disable(f)
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonMuted,
		BandwidthRequested:  0,
		BandwidthDelta:      0,
		Bitrates:            bitrates,
		TargetLayer:         buffer.InvalidLayer,
		RequestLayerSpatial: buffer.InvalidLayerSpatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   0,
	}
	result = f.AllocateOptimal(nil, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)

	f.Mute(false, true)

	// pub muted should not consume any bandwidth
	f.PubMute(true)
	disable(f)
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonPubMuted,
		BandwidthRequested:  0,
		BandwidthDelta:      0,
		Bitrates:            bitrates,
		TargetLayer:         buffer.InvalidLayer,
		RequestLayerSpatial: buffer.InvalidLayerSpatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   0,
	}
	result = f.AllocateOptimal(nil, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)

	f.PubMute(false)

	// when max layers changes, target is opportunistic, but requested spatial layer should be at max
	f.SetMaxTemporalLayerSeen(buffer.DefaultMaxLayerTemporal)
	f.vls.SetMax(buffer.VideoLayer{Spatial: 1, Temporal: 3})
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonNone,
		BandwidthRequested:  bitrates[2][1],
		BandwidthDelta:      bitrates[2][1],
		BandwidthNeeded:     bitrates[1][3],
		Bitrates:            bitrates,
		TargetLayer:         buffer.DefaultMaxLayer,
		RequestLayerSpatial: f.vls.GetMax().Spatial,
		MaxLayer:            f.vls.GetMax(),
		DistanceToDesired:   -1,
	}
	result = f.AllocateOptimal(nil, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, buffer.DefaultMaxLayer, f.TargetLayer())

	// reset max layers for rest of the tests below
	f.vls.SetMax(buffer.DefaultMaxLayer)

	// when feed is dry and current is not valid, should set up for opportunistic forwarding
	// NOTE: feed is dry due to availableLayers = nil, some valid bitrates may be passed in here for testing purposes only
	disable(f)
	expectedTargetLayer := buffer.DefaultMaxLayer
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonNone,
		BandwidthRequested:  bitrates[2][1],
		BandwidthDelta:      0,
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   -0.5,
	}
	result = f.AllocateOptimal(nil, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	f.vls.SetTarget(buffer.VideoLayer{Spatial: 0, Temporal: 0})  // set to valid to trigger paths in tests below
	f.vls.SetCurrent(buffer.VideoLayer{Spatial: 0, Temporal: 3}) // set to valid to trigger paths in tests below

	// when feed is dry and current is valid, should stay at current
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  0,
		Temporal: 3,
	}
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonFeedDry,
		BandwidthRequested:  0,
		BandwidthDelta:      0 - bitrates[2][1],
		Bitrates:            emptyBitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   -0.75,
	}
	result = f.AllocateOptimal(nil, emptyBitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	f.vls.SetCurrent(buffer.InvalidLayer)

	// opportunistic target if feed is not dry and current is not valid, i. e. not forwarding
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonNone,
		BandwidthRequested:  bitrates[2][1],
		BandwidthDelta:      bitrates[2][1],
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         buffer.DefaultMaxLayer,
		RequestLayerSpatial: 1,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   -0.5,
	}
	result = f.AllocateOptimal([]int32{0, 1}, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, buffer.DefaultMaxLayer, f.TargetLayer())

	// when holding in above scenario, should choose the lowest available layer
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  1,
		Temporal: 0,
	}
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonNone,
		BandwidthRequested:  bitrates[1][0],
		BandwidthDelta:      bitrates[1][0] - bitrates[2][1],
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: 1,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   1.25,
	}
	result = f.AllocateOptimal([]int32{1, 2}, bitrates, true, true)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	// opportunistic target if feed is dry and current is not valid, i. e. not forwarding
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonNone,
		BandwidthRequested:  bitrates[2][1],
		BandwidthDelta:      bitrates[2][1] - bitrates[1][0],
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         buffer.DefaultMaxLayer,
		RequestLayerSpatial: 2,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   -0.5,
	}
	result = f.AllocateOptimal(nil, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, buffer.DefaultMaxLayer, f.TargetLayer())

	// when holding in above scenario, should choose layer 0
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  0,
		Temporal: 0,
	}
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonNone,
		BandwidthRequested:  bitrates[0][0],
		BandwidthDelta:      bitrates[0][0] - bitrates[2][1],
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: 0,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   2.25,
	}
	result = f.AllocateOptimal(nil, bitrates, true, true)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	// if feed is not dry and current is not locked, should be opportunistic (with and without overshoot)
	f.vls.SetTarget(buffer.InvalidLayer)
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonFeedDry,
		BandwidthRequested:  0,
		BandwidthDelta:      0 - bitrates[0][0],
		BandwidthNeeded:     0,
		Bitrates:            emptyBitrates,
		TargetLayer:         buffer.DefaultMaxLayer,
		RequestLayerSpatial: 1,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   -1.0,
	}
	result = f.AllocateOptimal([]int32{0, 1}, emptyBitrates, false, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, buffer.DefaultMaxLayer, f.TargetLayer())

	f.vls.SetTarget(buffer.InvalidLayer)
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  2,
		Temporal: buffer.DefaultMaxLayerTemporal,
	}
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonNone,
		BandwidthRequested:  bitrates[2][1],
		BandwidthDelta:      bitrates[2][1],
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: 1,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   -0.5,
	}
	result = f.AllocateOptimal([]int32{0, 1}, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	// switches request layer to highest available if feed is not dry and current is valid and current is not available
	f.vls.SetCurrent(buffer.VideoLayer{Spatial: 0, Temporal: 1})
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  1,
		Temporal: buffer.DefaultMaxLayerTemporal,
	}
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonNone,
		BandwidthRequested:  bitrates[1][3],
		BandwidthDelta:      bitrates[1][3] - bitrates[2][1],
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: 1,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   0.5,
	}
	result = f.AllocateOptimal([]int32{1}, bitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	// when holding in above scenario, should switch to lowest available layer
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  0,
		Temporal: 0,
	}
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonNone,
		BandwidthRequested:  bitrates[0][0],
		BandwidthDelta:      bitrates[0][0] - bitrates[1][3],
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: 0,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   2.25,
	}
	result = f.AllocateOptimal([]int32{0, 1}, bitrates, true, true)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	// stays the same if feed is not dry and current is valid, available and locked
	f.vls.SetMax(buffer.VideoLayer{Spatial: 0, Temporal: 1})
	f.vls.SetCurrent(buffer.VideoLayer{Spatial: 0, Temporal: 1})
	f.vls.SetRequestSpatial(0)
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  0,
		Temporal: 1,
	}
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonFeedDry,
		BandwidthRequested:  0,
		BandwidthDelta:      0 - bitrates[0][0],
		Bitrates:            emptyBitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: 0,
		MaxLayer:            f.vls.GetMax(),
		DistanceToDesired:   0.0,
	}
	result = f.AllocateOptimal([]int32{0}, emptyBitrates, true, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())
}

func TestForwarderProvisionalAllocate(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)
	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)
	f.SetMaxPublishedLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayerSeen(buffer.DefaultMaxLayerTemporal)

	// Reset to invalid layers for testing allocation from scratch
	disable(f)

	bitrates := Bitrates{
		{1, 2, 3, 4},
		{5, 6, 7, 8},
		{9, 10, 11, 12},
	}

	f.ProvisionalAllocatePrepare(nil, bitrates)

	isCandidate, usedBitrate := f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 0, Temporal: 0}, true, false)
	require.True(t, isCandidate)
	require.Equal(t, bitrates[0][0], usedBitrate)

	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 2, Temporal: 3}, true, false)
	require.True(t, isCandidate)
	require.Equal(t, bitrates[2][3]-bitrates[0][0], usedBitrate)

	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 0, Temporal: 3}, true, false)
	require.True(t, isCandidate)
	require.Equal(t, bitrates[0][3]-bitrates[2][3], usedBitrate)

	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 1, Temporal: 2}, true, false)
	require.True(t, isCandidate)
	require.Equal(t, bitrates[1][2]-bitrates[0][3], usedBitrate)

	// available not enough to reach (2, 2), allocating at (2, 2) should not succeed
	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][2]-bitrates[1][2]-1, buffer.VideoLayer{Spatial: 2, Temporal: 2}, true, false)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	// committing should set target to (1, 2)
	expectedTargetLayer := buffer.VideoLayer{
		Spatial:  1,
		Temporal: 2,
	}
	expectedResult := VideoAllocation{
		IsDeficient:         true,
		BandwidthRequested:  bitrates[1][2],
		BandwidthDelta:      bitrates[1][2],
		BandwidthNeeded:     bitrates[2][3],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   1.25,
	}
	result := f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	// when nothing fits and pausing disallowed, should allocate (0, 0)
	f.vls.SetTarget(buffer.InvalidLayer)
	f.ProvisionalAllocatePrepare(nil, bitrates)
	isCandidate, usedBitrate = f.ProvisionalAllocate(0, buffer.VideoLayer{Spatial: 0, Temporal: 0}, false, false)
	require.True(t, isCandidate)
	require.Equal(t, int64(1), usedBitrate)

	// committing should set target to (0, 0)
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  0,
		Temporal: 0,
	}
	expectedResult = VideoAllocation{
		IsDeficient:         true,
		BandwidthRequested:  bitrates[0][0],
		BandwidthDelta:      bitrates[0][0] - bitrates[1][2],
		BandwidthNeeded:     bitrates[2][3],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   2.75,
	}
	result = f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	//
	// Test allowOvershoot.
	// Max spatial set to 0 and layer 0 bit rates are not available.
	//
	f.SetMaxSpatialLayer(0)
	bitrates = Bitrates{
		{0, 0, 0, 0},
		{5, 6, 7, 8},
		{9, 10, 11, 12},
	}

	f.ProvisionalAllocatePrepare(nil, bitrates)

	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 0, Temporal: 0}, false, true)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	// overshoot should succeed
	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 2, Temporal: 3}, false, true)
	require.True(t, isCandidate)
	require.Equal(t, bitrates[2][3], usedBitrate)

	// overshoot should succeed - this should win as this is lesser overshoot
	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 1, Temporal: 3}, false, true)
	require.True(t, isCandidate)
	require.Equal(t, bitrates[1][3]-bitrates[2][3], usedBitrate)

	// committing should set target to (1, 3)
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  1,
		Temporal: 3,
	}
	expectedMaxLayer := buffer.VideoLayer{
		Spatial:  0,
		Temporal: 3,
	}
	expectedResult = VideoAllocation{
		BandwidthRequested:  bitrates[1][3],
		BandwidthDelta:      bitrates[1][3] - 1, // 1 is the last allocation bandwidth requested
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            expectedMaxLayer,
		DistanceToDesired:   -1.75,
	}
	result = f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	//
	// Even if overshoot is allowed, but if higher layers do not have bit rates, should continue with current layer.
	//
	bitrates = Bitrates{
		{0, 0, 0, 0},
		{0, 0, 0, 0},
		{0, 0, 0, 0},
	}

	f.vls.SetCurrent(buffer.VideoLayer{Spatial: 0, Temporal: 2})
	f.ProvisionalAllocatePrepare(nil, bitrates)

	// all the provisional allocations should not succeed because the feed is dry
	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 0, Temporal: 0}, false, true)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	// overshoot should not succeed
	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 2, Temporal: 3}, false, true)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	// overshoot should not succeed
	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 1, Temporal: 3}, false, true)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	// committing should set target to (0, 2), i. e. leave it at current for opportunistic forwarding
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  0,
		Temporal: 2,
	}
	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonFeedDry,
		BandwidthRequested:  bitrates[0][2],
		BandwidthDelta:      bitrates[0][2] - 8, // 8 is the last allocation bandwidth requested
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            expectedMaxLayer,
		DistanceToDesired:   1.0,
	}
	result = f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())

	//
	// Same case as above, but current is above max, so target should go to invalid
	//
	f.vls.SetCurrent(buffer.VideoLayer{Spatial: 1, Temporal: 2})
	f.ProvisionalAllocatePrepare(nil, bitrates)

	// all the provisional allocations below should not succeed because the feed is dry
	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 0, Temporal: 0}, false, true)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	// overshoot should not succeed
	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 2, Temporal: 3}, false, true)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	// overshoot should not succeed
	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 1, Temporal: 3}, false, true)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	expectedResult = VideoAllocation{
		PauseReason:         VideoPauseReasonFeedDry,
		BandwidthRequested:  0,
		BandwidthDelta:      0,
		Bitrates:            bitrates,
		TargetLayer:         buffer.InvalidLayer,
		RequestLayerSpatial: buffer.InvalidLayerSpatial,
		MaxLayer:            expectedMaxLayer,
		DistanceToDesired:   1.0,
	}
	result = f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, buffer.InvalidLayer, f.TargetLayer())
	require.Equal(t, buffer.InvalidLayer, f.CurrentLayer())
}

func TestForwarderProvisionalAllocateMute(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)
	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)

	// Reset to invalid layers for testing muted state
	disable(f)

	bitrates := Bitrates{
		{1, 2, 3, 4},
		{5, 6, 7, 8},
		{9, 10, 11, 12},
	}

	f.Mute(true, true)
	f.ProvisionalAllocatePrepare(nil, bitrates)

	isCandidate, usedBitrate := f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 0, Temporal: 0}, true, false)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	isCandidate, usedBitrate = f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 1, Temporal: 2}, true, true)
	require.False(t, isCandidate)
	require.Equal(t, int64(0), usedBitrate)

	// committing should set target to buffer.InvalidLayer as track is muted
	expectedResult := VideoAllocation{
		PauseReason:         VideoPauseReasonMuted,
		BandwidthRequested:  0,
		BandwidthDelta:      0,
		Bitrates:            bitrates,
		TargetLayer:         buffer.InvalidLayer,
		RequestLayerSpatial: buffer.InvalidLayerSpatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   0,
	}
	result := f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, buffer.InvalidLayer, f.TargetLayer())
}

func TestForwarderProvisionalAllocateGetCooperativeTransition(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)
	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)
	f.SetMaxPublishedLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayerSeen(buffer.DefaultMaxLayerTemporal)

	// Reset to invalid layers for testing cooperative transition from scratch
	disable(f)

	availableLayers := []int32{0, 1, 2}
	bitrates := Bitrates{
		{1, 2, 3, 4},
		{5, 6, 7, 8},
		{9, 10, 0, 0},
	}

	f.ProvisionalAllocatePrepare(availableLayers, bitrates)

	// from scratch (buffer.InvalidLayer) should give back layer (0, 0)
	expectedTransition := VideoTransition{
		From:           buffer.InvalidLayer,
		To:             buffer.VideoLayer{Spatial: 0, Temporal: 0},
		BandwidthDelta: 1,
	}
	transition, al, brs := f.ProvisionalAllocateGetCooperativeTransition(false)
	require.Equal(t, expectedTransition, transition)
	require.Equal(t, availableLayers, al)
	require.Equal(t, bitrates, brs)

	// committing should set target to (0, 0)
	expectedLayers := buffer.VideoLayer{Spatial: 0, Temporal: 0}
	expectedResult := VideoAllocation{
		IsDeficient:         true,
		BandwidthRequested:  1,
		BandwidthDelta:      1,
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedLayers,
		RequestLayerSpatial: expectedLayers.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   2.25,
	}
	result := f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedLayers, f.TargetLayer())

	// a higher target that is already streaming, just maintain it
	targetLayer := buffer.VideoLayer{Spatial: 2, Temporal: 1}
	f.vls.SetTarget(targetLayer)
	f.lastAllocation.BandwidthRequested = 10
	expectedTransition = VideoTransition{
		From:           targetLayer,
		To:             targetLayer,
		BandwidthDelta: 0,
	}
	transition, al, brs = f.ProvisionalAllocateGetCooperativeTransition(false)
	require.Equal(t, expectedTransition, transition)
	require.Equal(t, availableLayers, al)
	require.Equal(t, bitrates, brs)

	// committing should set target to (2, 1)
	expectedLayers = buffer.VideoLayer{Spatial: 2, Temporal: 1}
	expectedResult = VideoAllocation{
		BandwidthRequested:  10,
		BandwidthDelta:      0,
		Bitrates:            bitrates,
		BandwidthNeeded:     bitrates[2][1],
		TargetLayer:         expectedLayers,
		RequestLayerSpatial: expectedLayers.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   0.0,
	}
	result = f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedLayers, f.TargetLayer())

	// from a target that has become unavailable, should switch to lower available layer
	targetLayer = buffer.VideoLayer{Spatial: 2, Temporal: 2}
	f.vls.SetTarget(targetLayer)
	expectedTransition = VideoTransition{
		From:           targetLayer,
		To:             buffer.VideoLayer{Spatial: 2, Temporal: 1},
		BandwidthDelta: 0,
	}
	transition, al, brs = f.ProvisionalAllocateGetCooperativeTransition(false)
	require.Equal(t, expectedTransition, transition)
	require.Equal(t, availableLayers, al)
	require.Equal(t, bitrates, brs)

	f.ProvisionalAllocateCommit()

	// mute
	f.Mute(true, true)
	f.ProvisionalAllocatePrepare(availableLayers, bitrates)

	// mute should send target to buffer.InvalidLayer
	expectedTransition = VideoTransition{
		From:           buffer.VideoLayer{Spatial: 2, Temporal: 1},
		To:             buffer.InvalidLayer,
		BandwidthDelta: -10,
	}
	transition, al, brs = f.ProvisionalAllocateGetCooperativeTransition(false)
	require.Equal(t, expectedTransition, transition)
	require.Equal(t, availableLayers, al)
	require.Equal(t, bitrates, brs)

	f.ProvisionalAllocateCommit()

	//
	// Test allowOvershoot
	//
	f.Mute(false, true)
	f.SetMaxSpatialLayer(0)

	availableLayers = []int32{1, 2}
	bitrates = Bitrates{
		{0, 0, 0, 0},
		{5, 6, 7, 8},
		{9, 10, 0, 0},
	}

	f.vls.SetTarget(buffer.InvalidLayer)
	f.ProvisionalAllocatePrepare(availableLayers, bitrates)

	// from scratch (buffer.InvalidLayer) should go to a layer past maximum as overshoot is allowed
	expectedTransition = VideoTransition{
		From:           buffer.InvalidLayer,
		To:             buffer.VideoLayer{Spatial: 1, Temporal: 0},
		BandwidthDelta: 5,
	}
	transition, al, brs = f.ProvisionalAllocateGetCooperativeTransition(true)
	require.Equal(t, expectedTransition, transition)
	require.Equal(t, availableLayers, al)
	require.Equal(t, bitrates, brs)

	// committing should set target to (1, 0)
	expectedLayers = buffer.VideoLayer{Spatial: 1, Temporal: 0}
	expectedMaxLayer := buffer.VideoLayer{Spatial: 0, Temporal: buffer.DefaultMaxLayerTemporal}
	expectedResult = VideoAllocation{
		BandwidthRequested:  5,
		BandwidthDelta:      5,
		Bitrates:            bitrates,
		TargetLayer:         expectedLayers,
		RequestLayerSpatial: expectedLayers.Spatial,
		MaxLayer:            expectedMaxLayer,
		DistanceToDesired:   -1.0,
	}
	result = f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedLayers, f.TargetLayer())

	//
	// Test continuing at current layers when feed is dry
	//
	bitrates = Bitrates{
		{0, 0, 0, 0},
		{0, 0, 0, 0},
		{0, 0, 0, 0},
	}

	f.vls.SetCurrent(buffer.VideoLayer{Spatial: 0, Temporal: 2})
	f.vls.SetTarget(buffer.InvalidLayer)
	f.ProvisionalAllocatePrepare(nil, bitrates)

	// from scratch (buffer.InvalidLayer) should go to current layer
	// NOTE: targetLayer is set to buffer.InvalidLayer for testing, but in practice current layers valid and target layers invalid should not happen
	expectedTransition = VideoTransition{
		From:           buffer.InvalidLayer,
		To:             buffer.VideoLayer{Spatial: 0, Temporal: 2},
		BandwidthDelta: -5, // 5 was the bandwidth needed for the last allocation
	}
	transition, al, brs = f.ProvisionalAllocateGetCooperativeTransition(true)
	require.Equal(t, expectedTransition, transition)
	require.Equal(t, []int32{}, al)
	require.Equal(t, bitrates, brs)

	// committing should set target to (0, 2)
	expectedLayers = buffer.VideoLayer{Spatial: 0, Temporal: 2}
	expectedResult = VideoAllocation{
		BandwidthRequested:  0,
		BandwidthDelta:      -5,
		Bitrates:            bitrates,
		TargetLayer:         expectedLayers,
		RequestLayerSpatial: expectedLayers.Spatial,
		MaxLayer:            expectedMaxLayer,
		DistanceToDesired:   -0.5,
	}
	result = f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedLayers, f.TargetLayer())

	// committing should set target to current layers to enable opportunistic forwarding
	expectedResult = VideoAllocation{
		BandwidthRequested:  0,
		BandwidthDelta:      0,
		Bitrates:            bitrates,
		TargetLayer:         expectedLayers,
		RequestLayerSpatial: expectedLayers.Spatial,
		MaxLayer:            expectedMaxLayer,
		DistanceToDesired:   -0.5,
	}
	result = f.ProvisionalAllocateCommit()
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedLayers, f.TargetLayer())
}

func TestForwarderProvisionalAllocateGetBestWeightedTransition(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)
	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)

	availableLayers := []int32{0, 1, 2}
	bitrates := Bitrates{
		{1, 2, 3, 4},
		{5, 6, 7, 8},
		{9, 10, 11, 12},
	}

	f.ProvisionalAllocatePrepare(availableLayers, bitrates)

	f.vls.SetTarget(buffer.VideoLayer{Spatial: 2, Temporal: 2})
	f.lastAllocation.BandwidthRequested = bitrates[2][2]
	expectedTransition := VideoTransition{
		From:           f.TargetLayer(),
		To:             buffer.VideoLayer{Spatial: 2, Temporal: 0},
		BandwidthDelta: -2,
	}
	transition, al, brs := f.ProvisionalAllocateGetBestWeightedTransition()
	require.Equal(t, expectedTransition, transition)
	require.Equal(t, availableLayers, al)
	require.Equal(t, bitrates, brs)
}

func TestForwarderAllocateNextHigher(t *testing.T) {
	f := newForwarder(testutils.TestOpusCodec, webrtc.RTPCodecTypeAudio)
	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)
	f.SetMaxPublishedLayer(buffer.DefaultMaxLayerSpatial)

	emptyBitrates := Bitrates{}
	bitrates := Bitrates{
		{2, 3, 0, 0},
		{4, 0, 0, 5},
		{0, 7, 0, 0},
	}

	result, boosted := f.AllocateNextHigher(100_000_000, nil, bitrates, false)
	require.Equal(t, VideoAllocationDefault, result) // no layer for audio
	require.False(t, boosted)

	f = newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)
	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)
	f.SetMaxPublishedLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayerSeen(buffer.DefaultMaxLayerTemporal)

	// when not in deficient state, does not boost
	result, boosted = f.AllocateNextHigher(100_000_000, nil, bitrates, false)
	require.Equal(t, VideoAllocationDefault, result)
	require.False(t, boosted)

	// if layers have not caught up, should not allocate next layer even if deficient
	f.vls.SetTarget(buffer.VideoLayer{
		Spatial:  0,
		Temporal: 0,
	})
	result, boosted = f.AllocateNextHigher(100_000_000, nil, bitrates, false)
	require.Equal(t, VideoAllocationDefault, result)
	require.False(t, boosted)

	f.lastAllocation.IsDeficient = true
	f.vls.SetCurrent(buffer.VideoLayer{
		Spatial:  0,
		Temporal: 0,
	})

	// move from (0, 0) -> (0, 1), i.e. a higher temporal layer is available in the same spatial layer
	expectedTargetLayer := buffer.VideoLayer{
		Spatial:  0,
		Temporal: 1,
	}
	expectedResult := VideoAllocation{
		IsDeficient:         true,
		BandwidthRequested:  3,
		BandwidthDelta:      1,
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   2.0,
	}
	result, boosted = f.AllocateNextHigher(100_000_000, nil, bitrates, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())
	require.True(t, boosted)

	// empty bitrates cannot increase layer, i. e. last allocation is left unchanged
	result, boosted = f.AllocateNextHigher(100_000_000, nil, emptyBitrates, false)
	require.Equal(t, expectedResult, result)
	require.False(t, boosted)

	// move from (0, 1) -> (1, 0), i.e. a higher spatial layer is available
	f.vls.SetCurrent(buffer.VideoLayer{Spatial: f.vls.GetCurrent().Spatial, Temporal: 1})
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  1,
		Temporal: 0,
	}
	expectedResult = VideoAllocation{
		IsDeficient:         true,
		BandwidthRequested:  4,
		BandwidthDelta:      1,
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   1.25,
	}
	result, boosted = f.AllocateNextHigher(100_000_000, nil, bitrates, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())
	require.True(t, boosted)

	// next higher, move from (1, 0) -> (1, 3), still deficient though
	f.vls.SetCurrent(buffer.VideoLayer{Spatial: 1, Temporal: 0})
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  1,
		Temporal: 3,
	}
	expectedResult = VideoAllocation{
		IsDeficient:         true,
		BandwidthRequested:  5,
		BandwidthDelta:      1,
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   0.5,
	}
	result, boosted = f.AllocateNextHigher(100_000_000, nil, bitrates, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())
	require.True(t, boosted)

	// next higher, move from (1, 3) -> (2, 1), optimal allocation
	f.vls.SetCurrent(buffer.VideoLayer{Spatial: f.vls.GetCurrent().Spatial, Temporal: 3})
	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  2,
		Temporal: 1,
	}
	expectedResult = VideoAllocation{
		BandwidthRequested:  7,
		BandwidthDelta:      2,
		Bitrates:            bitrates,
		BandwidthNeeded:     bitrates[2][1],
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   0.0,
	}
	result, boosted = f.AllocateNextHigher(100_000_000, nil, bitrates, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())
	require.True(t, boosted)

	// ask again, should return not boosted as there is no room to go higher
	f.vls.SetCurrent(buffer.VideoLayer{Spatial: 2, Temporal: 1})
	result, boosted = f.AllocateNextHigher(100_000_000, nil, bitrates, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())
	require.False(t, boosted)

	// turn off everything, allocating next layer should result in streaming lowest layers
	disable(f)
	f.lastAllocation.IsDeficient = true
	f.lastAllocation.BandwidthRequested = 0

	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  0,
		Temporal: 0,
	}
	expectedResult = VideoAllocation{
		IsDeficient:         true,
		BandwidthRequested:  2,
		BandwidthDelta:      2,
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   2.25,
	}
	result, boosted = f.AllocateNextHigher(100_000_000, nil, bitrates, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())
	require.True(t, boosted)

	// no new available capacity cannot bump up layer
	expectedResult = VideoAllocation{
		IsDeficient:         true,
		BandwidthRequested:  2,
		BandwidthDelta:      2,
		BandwidthNeeded:     bitrates[2][1],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   2.25,
	}
	result, boosted = f.AllocateNextHigher(0, nil, bitrates, false)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())
	require.False(t, boosted)

	// test allowOvershoot
	f.SetMaxSpatialLayer(0)

	bitrates = Bitrates{
		{0, 0, 0, 0},
		{5, 6, 7, 8},
		{9, 10, 11, 12},
	}

	f.vls.SetCurrent(f.vls.GetTarget())

	expectedTargetLayer = buffer.VideoLayer{
		Spatial:  1,
		Temporal: 0,
	}
	expectedMaxLayer := buffer.VideoLayer{
		Spatial:  0,
		Temporal: buffer.DefaultMaxLayerTemporal,
	}
	expectedResult = VideoAllocation{
		BandwidthRequested:  bitrates[1][0],
		BandwidthDelta:      bitrates[1][0],
		Bitrates:            bitrates,
		TargetLayer:         expectedTargetLayer,
		RequestLayerSpatial: expectedTargetLayer.Spatial,
		MaxLayer:            expectedMaxLayer,
		DistanceToDesired:   -1.0,
	}
	// overshoot should return (1, 0) even if there is not enough capacity
	result, boosted = f.AllocateNextHigher(bitrates[1][0]-1, nil, bitrates, true)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, expectedTargetLayer, f.TargetLayer())
	require.True(t, boosted)
}

func TestForwarderPause(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)
	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)
	f.SetMaxPublishedLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayerSeen(buffer.DefaultMaxLayerTemporal)

	bitrates := Bitrates{
		{1, 2, 3, 4},
		{5, 6, 7, 8},
		{9, 10, 11, 12},
	}

	f.ProvisionalAllocatePrepare(nil, bitrates)
	f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 0, Temporal: 0}, true, false)
	// should have set target at (0, 0)
	f.ProvisionalAllocateCommit()

	expectedResult := VideoAllocation{
		PauseReason:         VideoPauseReasonBandwidth,
		IsDeficient:         true,
		BandwidthRequested:  0,
		BandwidthDelta:      0 - bitrates[0][0],
		BandwidthNeeded:     bitrates[2][3],
		Bitrates:            bitrates,
		TargetLayer:         buffer.InvalidLayer,
		RequestLayerSpatial: buffer.InvalidLayerSpatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   3.75,
	}
	result := f.Pause(nil, bitrates)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, buffer.InvalidLayer, f.TargetLayer())
}

func TestForwarderPauseMute(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)
	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)
	f.SetMaxPublishedLayer(buffer.DefaultMaxLayerSpatial)

	bitrates := Bitrates{
		{1, 2, 3, 4},
		{5, 6, 7, 8},
		{9, 10, 11, 12},
	}

	f.ProvisionalAllocatePrepare(nil, bitrates)
	f.ProvisionalAllocate(bitrates[2][3], buffer.VideoLayer{Spatial: 0, Temporal: 0}, true, true)
	// should have set target at (0, 0)
	f.ProvisionalAllocateCommit()

	f.Mute(true, true)
	expectedResult := VideoAllocation{
		PauseReason:         VideoPauseReasonMuted,
		BandwidthRequested:  0,
		BandwidthDelta:      0 - bitrates[0][0],
		Bitrates:            bitrates,
		TargetLayer:         buffer.InvalidLayer,
		RequestLayerSpatial: buffer.InvalidLayerSpatial,
		MaxLayer:            buffer.DefaultMaxLayer,
		DistanceToDesired:   0,
	}
	result := f.Pause(nil, bitrates)
	require.Equal(t, expectedResult, result)
	require.Equal(t, expectedResult, f.lastAllocation)
	require.Equal(t, buffer.InvalidLayer, f.TargetLayer())
}

func TestForwarderGetTranslationParamsMuted(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)
	f.Mute(true, true)

	params := &testutils.TestExtPacketParams{
		SequenceNumber: 23333,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
	}
	extPkt, err := testutils.GetTestExtPacket(params)
	require.NoError(t, err)
	require.NotNil(t, extPkt)

	expectedTP := TranslationParams{
		shouldDrop: true,
	}
	actualTP, err := f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)
}

func TestForwarderGetTranslationParamsAudio(t *testing.T) {
	f := newForwarder(testutils.TestOpusCodec, webrtc.RTPCodecTypeAudio)

	params := &testutils.TestExtPacketParams{
		SequenceNumber: 23332,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
		IsOutOfOrder:   true,
	}
	extPkt, _ := testutils.GetTestExtPacket(params)

	// should not start on an out-of-order packet
	expectedTP := TranslationParams{
		shouldDrop: true,
	}
	actualTP, err := f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)
	require.False(t, f.started)
	require.Zero(t, f.lastSSRC)

	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23333,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	// should lock onto the first in-order packet
	expectedTP = TranslationParams{
		isStarting: true,
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingContiguous,
			extSequenceNumber: 23333,
			extTimestamp:      0xabcdef,
		},
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)
	require.True(t, f.started)
	require.Equal(t, f.lastSSRC, params.SSRC)

	// send a duplicate, should be dropped
	expectedTP = TranslationParams{
		shouldDrop: true,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// add a missing sequence number to the cache
	err = f.rtpMunger.snRangeMap.ExcludeRange(23334, 23335)
	require.NoError(t, err)

	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23336,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	_, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)

	// out-of-order packet should get offset from cache
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23335,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingOutOfOrder,
			extSequenceNumber: 23334,
			extTimestamp:      0xabcdef,
		},
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// padding only packet in order should be dropped
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23337,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	expectedTP = TranslationParams{
		shouldDrop: true,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// in order packet should be forwarded
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23338,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingContiguous,
			extSequenceNumber: 23336,
			extTimestamp:      0xabcdef,
		},
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// padding only packet after a gap should not be dropped
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23340,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingGap,
			extSequenceNumber: 23338,
			extTimestamp:      0xabcdef,
		},
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// out-of-order should be forwarded using cache
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23336,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingOutOfOrder,
			extSequenceNumber: 23335,
			extTimestamp:      0xabcdef,
		},
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// switching source should lock onto the new source, but sequence number should be contiguous
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 123,
		Timestamp:      0xfedcba,
		SSRC:           0x87654321,
		PayloadSize:    20,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingContiguous,
			extSequenceNumber: 23339,
			extTimestamp:      0xabcdf0,
		},
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)
	require.Equal(t, f.lastSSRC, params.SSRC)
}

func TestForwarderGetTranslationParamsVideo(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)

	params := &testutils.TestExtPacketParams{
		SequenceNumber: 23332,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
		Marker:         true,
		IsOutOfOrder:   true,
	}
	vp8 := &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13467,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: false,
	}
	extPkt, _ := testutils.GetTestExtPacketVP8(params, vp8)

	// should not start on an out-of-order packet
	expectedTP := TranslationParams{
		shouldDrop: true,
	}
	actualTP, err := f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)
	require.False(t, f.started)
	require.Zero(t, f.lastSSRC)

	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23333,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
		Marker:         true,
	}
	vp8 = &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13467,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: false,
	}
	extPkt, _ = testutils.GetTestExtPacketVP8(params, vp8)

	// no target layers, should drop
	expectedTP = TranslationParams{
		shouldDrop: true,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// although target layer matches, not a key frame, so should drop
	f.vls.SetTarget(buffer.VideoLayer{
		Spatial:  0,
		Temporal: 1,
	})
	expectedTP = TranslationParams{
		shouldDrop: true,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// should lock onto packet (key frame)
	vp8 = &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13467,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	extPkt, _ = testutils.GetTestExtPacketVP8(params, vp8)
	expectedVP8 := &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13467,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	marshalledVP8, _ := expectedVP8.Marshal()
	expectedTP = TranslationParams{
		isStarting:  true,
		isSwitching: true,
		isResuming:  true,
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingContiguous,
			extSequenceNumber: 23333,
			extTimestamp:      0xabcdef,
		},
		incomingHeaderSize: 6,
		codecBytes:         marshalledVP8,
		marker:             true,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)
	require.True(t, f.started)
	require.Equal(t, f.lastSSRC, params.SSRC)

	// send a duplicate, should be dropped
	expectedTP = TranslationParams{
		shouldDrop: true,
		marker:     true,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// out-of-order packet not in cache should be dropped
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23332,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	extPkt, _ = testutils.GetTestExtPacketVP8(params, vp8)
	expectedTP = TranslationParams{
		shouldDrop: true,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// padding only packet in order should be dropped
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23334,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
	}
	extPkt, _ = testutils.GetTestExtPacketVP8(params, vp8)
	expectedTP = TranslationParams{
		shouldDrop: true,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// in order packet should be forwarded
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23335,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	extPkt, _ = testutils.GetTestExtPacketVP8(params, vp8)
	expectedVP8 = &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13467,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	marshalledVP8, err = expectedVP8.Marshal()
	require.NoError(t, err)
	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingContiguous,
			extSequenceNumber: 23334,
			extTimestamp:      0xabcdef,
		},
		incomingHeaderSize: 6,
		codecBytes:         marshalledVP8,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// temporal layer matching target, should be forwarded
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23336,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	vp8 = &codec.VP8{
		FirstByte:  25,
		S:          true,
		I:          true,
		M:          true,
		PictureID:  13468,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        1,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	extPkt, _ = testutils.GetTestExtPacketVP8(params, vp8)
	expectedVP8 = &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13468,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        1,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	marshalledVP8, err = expectedVP8.Marshal()
	require.NoError(t, err)
	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingContiguous,
			extSequenceNumber: 23335,
			extTimestamp:      0xabcdef,
		},
		incomingHeaderSize: 6,
		codecBytes:         marshalledVP8,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// temporal layer higher than target, should be dropped
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23337,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	vp8 = &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13468,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        2,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	extPkt, _ = testutils.GetTestExtPacketVP8(params, vp8)
	expectedTP = TranslationParams{
		shouldDrop: true,
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingContiguous,
			extSequenceNumber: 23336,
			extTimestamp:      0xabcdef,
		},
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// RTP sequence number and VP8 picture id should be contiguous after dropping higher temporal layer picture
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23338,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	vp8 = &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13469,
		L:          true,
		TL0PICIDX:  234,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: false,
	}
	extPkt, _ = testutils.GetTestExtPacketVP8(params, vp8)
	expectedVP8 = &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13469,
		L:          true,
		TL0PICIDX:  234,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: false,
	}
	marshalledVP8, err = expectedVP8.Marshal()
	require.NoError(t, err)
	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingContiguous,
			extSequenceNumber: 23336,
			extTimestamp:      0xabcdef,
		},
		incomingHeaderSize: 6,
		codecBytes:         marshalledVP8,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// padding only packet after a gap should be forwarded
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23340,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingGap,
			extSequenceNumber: 23338,
			extTimestamp:      0xabcdef,
		},
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// out-of-order should be forwarded using cache, even if it is padding only
	params = &testutils.TestExtPacketParams{
		SequenceNumber: 23339,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
	}
	extPkt, _ = testutils.GetTestExtPacket(params)

	expectedTP = TranslationParams{
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingOutOfOrder,
			extSequenceNumber: 23337,
			extTimestamp:      0xabcdef,
		},
	}
	actualTP, err = f.GetTranslationParams(extPkt, 0)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)

	// switching SSRC (happens for new layer or new track source)
	// should lock onto the new source, but sequence number should be contiguous
	f.vls.SetTarget(buffer.VideoLayer{
		Spatial:  1,
		Temporal: 1,
	})

	params = &testutils.TestExtPacketParams{
		SequenceNumber: 123,
		Timestamp:      0xfedcba,
		SSRC:           0x87654321,
		PayloadSize:    20,
	}
	vp8 = &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          false,
		PictureID:  45,
		L:          true,
		TL0PICIDX:  12,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     30,
		HeaderSize: 5,
		IsKeyFrame: true,
	}
	extPkt, _ = testutils.GetTestExtPacketVP8(params, vp8)

	expectedVP8 = &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13470,
		L:          true,
		TL0PICIDX:  235,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     24,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	marshalledVP8, err = expectedVP8.Marshal()
	require.NoError(t, err)
	expectedTP = TranslationParams{
		isSwitching: true,
		rtp: TranslationParamsRTP{
			snOrdering:        SequenceNumberOrderingContiguous,
			extSequenceNumber: 23339,
			extTimestamp:      0xabcdf0,
		},
		incomingHeaderSize: 5,
		codecBytes:         marshalledVP8,
	}
	actualTP, err = f.GetTranslationParams(extPkt, 1)
	require.NoError(t, err)
	require.Equal(t, expectedTP, actualTP)
	require.Equal(t, f.lastSSRC, params.SSRC)
}

func TestForwarderGetSnTsForPadding(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)

	params := &testutils.TestExtPacketParams{
		SequenceNumber: 23333,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	vp8 := &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13467,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	extPkt, _ := testutils.GetTestExtPacketVP8(params, vp8)

	f.vls.SetTarget(buffer.VideoLayer{
		Spatial:  0,
		Temporal: 1,
	})
	f.vls.SetCurrent(buffer.InvalidLayer)

	// send it through so that forwarder locks onto stream
	_, _ = f.GetTranslationParams(extPkt, 0)

	// pause stream and get padding, it should still work
	disable(f)

	// should get back frame end needed as the last packet did not have RTP marker set
	snts, err := f.GetSnTsForPadding(5, 0, false)
	require.NoError(t, err)

	numPadding := 5
	clockRate := uint32(0)
	frameRate := uint32(5)
	var sntsExpected = make([]SnTs, numPadding)
	for i := range numPadding {
		sntsExpected[i] = SnTs{
			extSequenceNumber: 23333 + uint64(i) + 1,
			extTimestamp:      0xabcdef + (uint64(i)*uint64(clockRate))/uint64(frameRate),
		}
	}
	require.Equal(t, sntsExpected, snts)

	// now that there is a marker, timestamp should jump on first padding when asked again
	snts, err = f.GetSnTsForPadding(numPadding, 0, false)
	require.NoError(t, err)

	for i := range numPadding {
		sntsExpected[i] = SnTs{
			extSequenceNumber: 23338 + uint64(i) + 1,
			extTimestamp:      0xabcdef + (uint64(i+1)*uint64(clockRate))/uint64(frameRate),
		}
	}
	require.Equal(t, sntsExpected, snts)
}

func TestForwarderGetSnTsForBlankFrames(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)

	params := &testutils.TestExtPacketParams{
		SequenceNumber: 23333,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	vp8 := &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13467,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	extPkt, _ := testutils.GetTestExtPacketVP8(params, vp8)

	f.vls.SetTarget(buffer.VideoLayer{
		Spatial:  0,
		Temporal: 1,
	})
	f.vls.SetCurrent(buffer.InvalidLayer)

	// send it through so that forwarder locks onto stream
	_, _ = f.GetTranslationParams(extPkt, 0)

	// should get back frame end needed as the last packet did not have RTP marker set
	numBlankFrames := 6
	snts, frameEndNeeded, err := f.GetSnTsForBlankFrames(30, numBlankFrames)
	require.NoError(t, err)
	require.True(t, frameEndNeeded)

	// there should be one more than RTPBlankFramesMax as one would have been allocated to end previous frame
	numPadding := numBlankFrames + 1
	clockRate := testutils.TestVP8Codec.ClockRate
	frameRate := uint32(30)
	var sntsExpected = make([]SnTs, numPadding)
	for i := 0; i < numPadding; i++ {
		// first blank frame should have same timestamp as last frame as end frame is synthesized
		ts := params.Timestamp
		if i != 0 {
			// +1 here due to expected time stamp bumpint by at least one so that time stamp is always moving ahead
			ts = params.Timestamp + 1 + ((uint32(i)*clockRate)+frameRate-1)/frameRate
		}
		sntsExpected[i] = SnTs{
			extSequenceNumber: uint64(params.SequenceNumber) + uint64(i) + 1,
			extTimestamp:      uint64(ts),
		}
	}
	require.Equal(t, sntsExpected, snts)

	// now that there is a marker, timestamp should jump on first padding when asked again
	// also number of padding should be RTPBlankFramesMax
	numPadding = numBlankFrames
	sntsExpected = sntsExpected[:numPadding]
	for i := 0; i < numPadding; i++ {
		sntsExpected[i] = SnTs{
			extSequenceNumber: uint64(params.SequenceNumber) + uint64(len(snts)) + uint64(i) + 1,
			// +1 here due to expected time stamp bumpint by at least one so that time stamp is always moving ahead
			extTimestamp: snts[len(snts)-1].extTimestamp + 1 + ((uint64(i+1)*uint64(clockRate))+uint64(frameRate)-1)/uint64(frameRate),
		}
	}
	snts, frameEndNeeded, err = f.GetSnTsForBlankFrames(30, numBlankFrames)
	require.NoError(t, err)
	require.False(t, frameEndNeeded)
	require.Equal(t, sntsExpected, snts)
}

func TestForwarderGetPaddingVP8(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)

	params := &testutils.TestExtPacketParams{
		SequenceNumber: 23333,
		Timestamp:      0xabcdef,
		SSRC:           0x12345678,
		PayloadSize:    20,
	}
	vp8 := &codec.VP8{
		FirstByte:  25,
		I:          true,
		M:          true,
		PictureID:  13467,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        13,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	extPkt, _ := testutils.GetTestExtPacketVP8(params, vp8)

	f.vls.SetTarget(buffer.VideoLayer{
		Spatial:  0,
		Temporal: 1,
	})
	f.vls.SetCurrent(buffer.InvalidLayer)

	// send it through so that forwarder locks onto stream
	_, _ = f.GetTranslationParams(extPkt, 0)

	// getting padding with frame end needed, should repeat the last picture id
	expectedVP8 := codec.VP8{
		FirstByte:  16,
		I:          true,
		M:          true,
		PictureID:  13467,
		L:          true,
		TL0PICIDX:  233,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     23,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	buf, err := f.GetPadding(true)
	require.NoError(t, err)
	marshalledVP8, err := expectedVP8.Marshal()
	require.NoError(t, err)
	require.Equal(t, marshalledVP8, buf)

	// getting padding with no frame end needed, should get next picture id
	expectedVP8 = codec.VP8{
		FirstByte:  16,
		I:          true,
		M:          true,
		PictureID:  13468,
		L:          true,
		TL0PICIDX:  234,
		T:          true,
		TID:        0,
		Y:          true,
		K:          true,
		KEYIDX:     24,
		HeaderSize: 6,
		IsKeyFrame: true,
	}
	buf, err = f.GetPadding(false)
	require.NoError(t, err)
	marshalledVP8, err = expectedVP8.Marshal()
	require.NoError(t, err)
	require.Equal(t, marshalledVP8, buf)
}

func TestForwarderInitialAcquisitionGrace(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)

	// subscriber requested the top spatial layer
	f.SetMaxSpatialLayer(buffer.DefaultMaxLayerSpatial)
	f.SetMaxTemporalLayer(buffer.DefaultMaxLayerTemporal)
	f.SetMaxTemporalLayerSeen(buffer.DefaultMaxLayerTemporal)

	bitrates := Bitrates{
		{2, 3, 0, 0},
		{4, 0, 0, 5},
		{0, 7, 0, 0},
	}

	// subscriber-first: only layer 1 has been seen so far and nothing is being forwarded yet
	// (current invalid). this arms the initial-acquisition grace.
	require.True(t, f.SetMaxPublishedLayer(1))
	f.lock.RLock()
	require.True(t, f.withinAcquireGraceLocked())
	f.lock.RUnlock()

	// during the grace, the target aims straight at the requested layer (2) and requests a key
	// frame for it, even though only layer 1 has been seen - so acquisition does not ramp up
	// gradually as higher layers are detected
	alloc := f.AllocateOptimal([]int32{0, 1}, bitrates, false, false)
	require.Equal(t, int32(2), alloc.TargetLayer.Spatial)
	require.Equal(t, int32(2), alloc.RequestLayerSpatial)

	// force the grace to expire while still not streaming: must signal that a re-allocation is
	// needed so the target can fall back
	f.acquireDeadline = 1 // a deadline far in the past
	require.True(t, f.MaybeExpireAcquireGrace())
	require.False(t, f.MaybeExpireAcquireGrace()) // only fires once

	// after the grace, the target falls back to the highest layer actually seen (1) instead of
	// stalling while waiting for a requested layer that never showed up
	alloc = f.AllocateOptimal([]int32{0, 1}, bitrates, false, false)
	require.Equal(t, int32(1), alloc.TargetLayer.Spatial)
	require.Equal(t, int32(1), alloc.RequestLayerSpatial)
}

func TestGetRefLayerRTPTimestampBounds(t *testing.T) {
	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)

	layerCount := int32(len(f.refInfos))

	_, err := f.getRefLayerRTPTimestamp(1000, layerCount, 0)
	require.Error(t, err)

	_, err = f.getRefLayerRTPTimestamp(1000, 0, layerCount)
	require.Error(t, err)

	_, err = f.getRefLayerRTPTimestamp(1000, -1, 0)
	require.Error(t, err)

	_, err = f.getRefLayerRTPTimestamp(1000, 0, -1)
	require.Error(t, err)

	// same-layer translation does not need sender reports
	ts, err := f.getRefLayerRTPTimestamp(1000, 0, 0)
	require.NoError(t, err)
	require.Equal(t, uint32(1000), ts)

	// last valid index must be accepted by the bounds check (may still error for missing SR)
	_, err = f.getRefLayerRTPTimestamp(1000, layerCount-1, 0)
	require.Error(t, err) // unavailable sender report, not invalid layer
	require.Contains(t, err.Error(), "unavailable")
}

// TestForwarderIsEndOfLayerFrame checks the end-of-layer-frame detection used to
// locate packet trailers, which VP9 SVC can carry at the end of any spatial layer
// frame and not just at the end of a picture.
func TestForwarderIsEndOfLayerFrame(t *testing.T) {
	vp9Codec := webrtc.RTPCodecCapability{MimeType: mime.MimeTypeVP9.String(), ClockRate: 90000}

	f := newForwarder(testutils.TestVP8Codec, webrtc.RTPCodecTypeVideo)
	require.False(t, f.isEndOfLayerFrame(&buffer.ExtPacket{Payload: codecs.VP9Packet{E: true}}))

	f = newForwarder(vp9Codec, webrtc.RTPCodecTypeVideo)
	require.True(t, f.isEndOfLayerFrame(&buffer.ExtPacket{Payload: codecs.VP9Packet{E: true}}))
	require.False(t, f.isEndOfLayerFrame(&buffer.ExtPacket{Payload: codecs.VP9Packet{E: false}}))
	require.False(t, f.isEndOfLayerFrame(&buffer.ExtPacket{}))

	require.True(t, f.isEndOfLayerFrame(&buffer.ExtPacket{
		DependencyDescriptor: &buffer.ExtDependencyDescriptor{
			Descriptor: &dd.DependencyDescriptor{LastPacketInFrame: true},
		},
		Payload: codecs.VP9Packet{E: false},
	}))
	require.False(t, f.isEndOfLayerFrame(&buffer.ExtPacket{
		DependencyDescriptor: &buffer.ExtDependencyDescriptor{
			Descriptor: &dd.DependencyDescriptor{LastPacketInFrame: false},
		},
		Payload: codecs.VP9Packet{E: true},
	}))
	require.False(t, f.isEndOfLayerFrame(&buffer.ExtPacket{
		DependencyDescriptor: &buffer.ExtDependencyDescriptor{},
	}))
}
