//go:build !js
// +build !js

package webrtc

import (
	"encoding/json"
	"fmt"
	"sync"
	"testing"
	"time"

	"github.com/pion/ice/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var errReceiveOfferTimeout = fmt.Errorf("timed out waiting to receive offer")

func TestStatsTimestampTime(t *testing.T) {
	for _, test := range []struct {
		Timestamp StatsTimestamp
		WantTime  time.Time
	}{
		{
			Timestamp: 0,
			WantTime:  time.Unix(0, 0),
		},
		{
			Timestamp: 1,
			WantTime:  time.Unix(0, 1e6),
		},
		{
			Timestamp: 0.001,
			WantTime:  time.Unix(0, 1e3),
		},
	} {
		if got, want := test.Timestamp.Time(), test.WantTime.UTC(); got != want {
			t.Fatalf("StatsTimestamp(%v).Time() = %v, want %v", test.Timestamp, got, want)
		}
	}
}

func TestStatsMarshal(t *testing.T) {
	for _, test := range []Stats{
		AudioReceiverStats{},
		AudioSenderStats{},
		CertificateStats{},
		CodecStats{},
		DataChannelStats{},
		ICECandidatePairStats{},
		ICECandidateStats{},
		InboundRTPStreamStats{},
		MediaStreamStats{},
		OutboundRTPStreamStats{},
		PeerConnectionStats{},
		RemoteInboundRTPStreamStats{},
		RemoteOutboundRTPStreamStats{},
		RTPContributingSourceStats{},
		SenderAudioTrackAttachmentStats{},
		SenderAudioTrackAttachmentStats{},
		SenderVideoTrackAttachmentStats{},
		TransportStats{},
		VideoReceiverStats{},
		VideoReceiverStats{},
		VideoSenderStats{},
	} {
		_, err := json.Marshal(test)
		if err != nil {
			t.Fatal(err)
		}
	}
}

func waitWithTimeout(t *testing.T, wg *sync.WaitGroup) {
	// Wait for all of the event handlers to be triggered.
	done := make(chan struct{})
	go func() {
		wg.Wait()
		done <- struct{}{}
	}()
	timeout := time.After(5 * time.Second)
	select {
	case <-done:
		break
	case <-timeout:
		t.Fatal("timed out waiting for waitgroup")
	}
}

func getConnectionStats(t *testing.T, report StatsReport, pc *PeerConnection) PeerConnectionStats {
	stats, ok := report.GetConnectionStats(pc)
	assert.True(t, ok)
	assert.Equal(t, stats.Type, StatsTypePeerConnection)
	return stats
}

func getDataChannelStats(t *testing.T, report StatsReport, dc *DataChannel) DataChannelStats {
	stats, ok := report.GetDataChannelStats(dc)
	assert.True(t, ok)
	assert.Equal(t, stats.Type, StatsTypeDataChannel)
	return stats
}

func getCodecStats(t *testing.T, report StatsReport, c *RTPCodecParameters) CodecStats {
	stats, ok := report.GetCodecStats(c)
	assert.True(t, ok)
	assert.Equal(t, stats.Type, StatsTypeCodec)
	return stats
}

func getTransportStats(t *testing.T, report StatsReport, statsID string) TransportStats {
	stats, ok := report[statsID]
	assert.True(t, ok)
	transportStats, ok := stats.(TransportStats)
	assert.True(t, ok)
	assert.Equal(t, transportStats.Type, StatsTypeTransport)
	return transportStats
}

func getCertificateStats(t *testing.T, report StatsReport, certificate *Certificate) CertificateStats {
	certificateStats, ok := report.GetCertificateStats(certificate)
	assert.True(t, ok)
	assert.Equal(t, certificateStats.Type, StatsTypeCertificate)
	return certificateStats
}

func findLocalCandidateStats(report StatsReport) []ICECandidateStats {
	result := []ICECandidateStats{}
	for _, s := range report {
		stats, ok := s.(ICECandidateStats)
		if ok && stats.Type == StatsTypeLocalCandidate {
			result = append(result, stats)
		}
	}
	return result
}

func findRemoteCandidateStats(report StatsReport) []ICECandidateStats {
	result := []ICECandidateStats{}
	for _, s := range report {
		stats, ok := s.(ICECandidateStats)
		if ok && stats.Type == StatsTypeRemoteCandidate {
			result = append(result, stats)
		}
	}
	return result
}

func findCandidatePairStats(t *testing.T, report StatsReport) []ICECandidatePairStats {
	result := []ICECandidatePairStats{}
	for _, s := range report {
		stats, ok := s.(ICECandidatePairStats)
		if ok {
			assert.Equal(t, StatsTypeCandidatePair, stats.Type)
			result = append(result, stats)
		}
	}
	return result
}

func signalPairForStats(pcOffer *PeerConnection, pcAnswer *PeerConnection) error {
	offerChan := make(chan SessionDescription)
	pcOffer.OnICECandidate(func(candidate *ICECandidate) {
		if candidate == nil {
			offerChan <- *pcOffer.PendingLocalDescription()
		}
	})

	offer, err := pcOffer.CreateOffer(nil)
	if err != nil {
		return err
	}
	if err := pcOffer.SetLocalDescription(offer); err != nil {
		return err
	}

	timeout := time.After(3 * time.Second)
	select {
	case <-timeout:
		return errReceiveOfferTimeout
	case offer := <-offerChan:
		if err := pcAnswer.SetRemoteDescription(offer); err != nil {
			return err
		}

		answer, err := pcAnswer.CreateAnswer(nil)
		if err != nil {
			return err
		}

		if err = pcAnswer.SetLocalDescription(answer); err != nil {
			return err
		}

		err = pcOffer.SetRemoteDescription(answer)
		if err != nil {
			return err
		}
		return nil
	}
}

func TestStatsConvertState(t *testing.T) {
	testCases := []struct {
		ice   ice.CandidatePairState
		stats StatsICECandidatePairState
	}{
		{
			ice.CandidatePairStateWaiting,
			StatsICECandidatePairStateWaiting,
		},
		{
			ice.CandidatePairStateInProgress,
			StatsICECandidatePairStateInProgress,
		},
		{
			ice.CandidatePairStateFailed,
			StatsICECandidatePairStateFailed,
		},
		{
			ice.CandidatePairStateSucceeded,
			StatsICECandidatePairStateSucceeded,
		},
	}

	s, err := toStatsICECandidatePairState(ice.CandidatePairState(42))

	assert.Error(t, err)
	assert.Equal(t,
		StatsICECandidatePairState("Unknown"),
		s)
	for i, testCase := range testCases {
		s, err := toStatsICECandidatePairState(testCase.ice)
		assert.NoError(t, err)
		assert.Equal(t,
			testCase.stats,
			s,
			"testCase: %d %v", i, testCase,
		)
	}
}

func TestPeerConnection_GetStats(t *testing.T) {
	offerPC, answerPC, err := newPair()
	assert.NoError(t, err)

	track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1")
	require.NoError(t, err)

	_, err = offerPC.AddTrack(track1)
	require.NoError(t, err)

	baseLineReportPCOffer := offerPC.GetStats()
	baseLineReportPCAnswer := answerPC.GetStats()

	connStatsOffer := getConnectionStats(t, baseLineReportPCOffer, offerPC)
	connStatsAnswer := getConnectionStats(t, baseLineReportPCAnswer, answerPC)

	for _, connStats := range []PeerConnectionStats{connStatsOffer, connStatsAnswer} {
		assert.Equal(t, uint32(0), connStats.DataChannelsOpened)
		assert.Equal(t, uint32(0), connStats.DataChannelsClosed)
		assert.Equal(t, uint32(0), connStats.DataChannelsRequested)
		assert.Equal(t, uint32(0), connStats.DataChannelsAccepted)
	}

	// Create a DC, open it and send a message
	offerDC, err := offerPC.CreateDataChannel("offerDC", nil)
	assert.NoError(t, err)

	msg := []byte("a classic test message")
	offerDC.OnOpen(func() {
		assert.NoError(t, offerDC.Send(msg))
	})

	dcWait := sync.WaitGroup{}
	dcWait.Add(1)

	answerDCChan := make(chan *DataChannel)
	answerPC.OnDataChannel(func(d *DataChannel) {
		d.OnOpen(func() {
			answerDCChan <- d
		})
		d.OnMessage(func(m DataChannelMessage) {
			dcWait.Done()
		})
	})

	assert.NoError(t, signalPairForStats(offerPC, answerPC))
	waitWithTimeout(t, &dcWait)

	answerDC := <-answerDCChan

	reportPCOffer := offerPC.GetStats()
	reportPCAnswer := answerPC.GetStats()

	connStatsOffer = getConnectionStats(t, reportPCOffer, offerPC)
	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsOpened)
	assert.Equal(t, uint32(0), connStatsOffer.DataChannelsClosed)
	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsRequested)
	assert.Equal(t, uint32(0), connStatsOffer.DataChannelsAccepted)
	dcStatsOffer := getDataChannelStats(t, reportPCOffer, offerDC)
	assert.Equal(t, DataChannelStateOpen, dcStatsOffer.State)
	assert.Equal(t, uint32(1), dcStatsOffer.MessagesSent)
	assert.Equal(t, uint64(len(msg)), dcStatsOffer.BytesSent)
	assert.NotEmpty(t, findLocalCandidateStats(reportPCOffer))
	assert.NotEmpty(t, findRemoteCandidateStats(reportPCOffer))
	assert.NotEmpty(t, findCandidatePairStats(t, reportPCOffer))

	connStatsAnswer = getConnectionStats(t, reportPCAnswer, answerPC)
	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsOpened)
	assert.Equal(t, uint32(0), connStatsAnswer.DataChannelsClosed)
	assert.Equal(t, uint32(0), connStatsAnswer.DataChannelsRequested)
	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsAccepted)
	dcStatsAnswer := getDataChannelStats(t, reportPCAnswer, answerDC)
	assert.Equal(t, DataChannelStateOpen, dcStatsAnswer.State)
	assert.Equal(t, uint32(1), dcStatsAnswer.MessagesReceived)
	assert.Equal(t, uint64(len(msg)), dcStatsAnswer.BytesReceived)
	assert.NotEmpty(t, findLocalCandidateStats(reportPCAnswer))
	assert.NotEmpty(t, findRemoteCandidateStats(reportPCAnswer))
	assert.NotEmpty(t, findCandidatePairStats(t, reportPCAnswer))
	assert.NoError(t, err)
	for i := range offerPC.api.mediaEngine.videoCodecs {
		codecStat := getCodecStats(t, reportPCOffer, &(offerPC.api.mediaEngine.videoCodecs[i]))
		assert.NotEmpty(t, codecStat)
	}
	for i := range offerPC.api.mediaEngine.audioCodecs {
		codecStat := getCodecStats(t, reportPCOffer, &(offerPC.api.mediaEngine.audioCodecs[i]))
		assert.NotEmpty(t, codecStat)
	}

	// Close answer DC now
	dcWait = sync.WaitGroup{}
	dcWait.Add(1)
	offerDC.OnClose(func() {
		dcWait.Done()
	})
	assert.NoError(t, answerDC.Close())
	waitWithTimeout(t, &dcWait)
	time.Sleep(10 * time.Millisecond)

	reportPCOffer = offerPC.GetStats()
	reportPCAnswer = answerPC.GetStats()

	connStatsOffer = getConnectionStats(t, reportPCOffer, offerPC)
	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsOpened)
	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsClosed)
	assert.Equal(t, uint32(1), connStatsOffer.DataChannelsRequested)
	assert.Equal(t, uint32(0), connStatsOffer.DataChannelsAccepted)
	dcStatsOffer = getDataChannelStats(t, reportPCOffer, offerDC)
	assert.Equal(t, DataChannelStateClosed, dcStatsOffer.State)

	connStatsAnswer = getConnectionStats(t, reportPCAnswer, answerPC)
	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsOpened)
	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsClosed)
	assert.Equal(t, uint32(0), connStatsAnswer.DataChannelsRequested)
	assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsAccepted)
	dcStatsAnswer = getDataChannelStats(t, reportPCAnswer, answerDC)
	assert.Equal(t, DataChannelStateClosed, dcStatsAnswer.State)

	answerICETransportStats := getTransportStats(t, reportPCAnswer, "iceTransport")
	offerICETransportStats := getTransportStats(t, reportPCOffer, "iceTransport")
	assert.GreaterOrEqual(t, offerICETransportStats.BytesSent, answerICETransportStats.BytesReceived)
	assert.GreaterOrEqual(t, answerICETransportStats.BytesSent, offerICETransportStats.BytesReceived)

	answerSCTPTransportStats := getTransportStats(t, reportPCAnswer, "sctpTransport")
	offerSCTPTransportStats := getTransportStats(t, reportPCOffer, "sctpTransport")
	assert.GreaterOrEqual(t, offerSCTPTransportStats.BytesSent, answerSCTPTransportStats.BytesReceived)
	assert.GreaterOrEqual(t, answerSCTPTransportStats.BytesSent, offerSCTPTransportStats.BytesReceived)

	certificates := offerPC.configuration.Certificates

	for i := range certificates {
		assert.NotEmpty(t, getCertificateStats(t, reportPCOffer, &certificates[i]))
	}

	closePairNow(t, offerPC, answerPC)
}

func TestPeerConnection_GetStats_Closed(t *testing.T) {
	pc, err := NewPeerConnection(Configuration{})
	assert.NoError(t, err)

	assert.NoError(t, pc.Close())

	pc.GetStats()
}
