package video

import (
	"fmt"
	"image"
	"runtime"
	"testing"
	"time"

	"github.com/pion/mediadevices/pkg/prop"
)

func BenchmarkDetectChanges(b *testing.B) {
	var src Reader
	src = ReaderFunc(func() (image.Image, func(), error) {
		return image.NewRGBA(image.Rect(0, 0, 1920, 1080)), func() {}, nil
	})

	b.Run("WithoutDetectChanges", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			src.Read()
		}
	})

	ns := []int{1, 8, 64, 256}
	for _, n := range ns {
		n := n
		src := src
		b.Run(fmt.Sprintf("WithDetectChanges%d", n), func(b *testing.B) {
			for i := 0; i < n; i++ {
				src = DetectChanges(time.Microsecond, func(p prop.Media) {})(src)
			}

			for i := 0; i < b.N; i++ {
				src.Read()
			}
		})
	}
}

func TestDetectChanges(t *testing.T) {
	buildSource := func(p prop.Media) (Reader, func(prop.Media)) {
		return ReaderFunc(func() (image.Image, func(), error) {
				return image.NewRGBA(image.Rect(0, 0, p.Width, p.Height)), func() {}, nil
			}), func(newProp prop.Media) {
				p = newProp
			}
	}

	assertEq := func(t *testing.T, actual prop.Media, expected prop.Media, output image.Image, assertFrameRate bool) {
		if actual.Height != expected.Height {
			t.Fatalf("expected height from to be %d but got %d", expected.Height, actual.Height)
		}

		if actual.Width != expected.Width {
			t.Fatalf("expected width from to be %d but got %d", expected.Width, actual.Width)
		}

		if assertFrameRate {
			diff := actual.FrameRate - expected.FrameRate
			// TODO: reduce this eps. Darwin CI keeps failing if we use a lower value
			var eps float32 = 1.5
			if diff < -eps || diff > eps {
				t.Fatalf("expected frame rate to be %f (+-%f) but got %f", expected.FrameRate, eps, actual.FrameRate)
			}
		}

		if output.Bounds().Dy() != expected.Height {
			t.Fatalf("expected output height from to be %d but got %d", expected.Height, output.Bounds().Dy())
		}

		if output.Bounds().Dx() != expected.Width {
			t.Fatalf("expected output width from to be %d but got %d", expected.Width, output.Bounds().Dx())
		}
	}

	t.Run("OnChangeCalledBeforeFirstFrame", func(t *testing.T) {
		var detectBeforeFirstFrame bool
		var expected prop.Media
		var actual prop.Media
		expected.Width = 1920
		expected.Height = 1080
		src, _ := buildSource(expected)
		src = DetectChanges(time.Second, func(p prop.Media) {
			actual = p
			detectBeforeFirstFrame = true
		})(src)

		frame, _, err := src.Read()
		if err != nil {
			t.Fatal(err)
		}

		if !detectBeforeFirstFrame {
			t.Fatal("on change callback should have called before first frame")
		}

		assertEq(t, actual, expected, frame, false)
	})

	t.Run("DetectChangesOnEveryUpdate", func(t *testing.T) {
		var expected prop.Media
		var actual prop.Media
		expected.Width = 1920
		expected.Height = 1080
		src, update := buildSource(expected)
		src = DetectChanges(time.Second, func(p prop.Media) {
			actual = p
		})(src)

		for width := 1920; width < 4000; width += 100 {
			for height := 1080; height < 2000; height += 100 {
				expected.Width = width
				expected.Height = height
				update(expected)
				frame, _, err := src.Read()
				if err != nil {
					t.Fatal(err)
				}

				assertEq(t, actual, expected, frame, false)
			}
		}
	})

	t.Run("FrameRateAccuracy", func(t *testing.T) {
		// https://github.com/pion/mediadevices/issues/198
		if runtime.GOOS == "darwin" {
			t.Skip("Skipping because Darwin CI is not reliable for timing related tests.")
		}

		var expected prop.Media
		var actual prop.Media
		var count int
		expected.Width = 1920
		expected.Height = 1080
		expected.FrameRate = 30
		src, _ := buildSource(expected)
		src = Throttle(expected.FrameRate)(src)
		src = DetectChanges(time.Second*5, func(p prop.Media) {
			actual = p
			count++
		})(src)

		for count < 3 {
			frame, _, err := src.Read()
			if err != nil {
				t.Fatal(err)
			}

			checkFrameRate := false
			if actual.FrameRate != 0.0 {
				checkFrameRate = true
			}
			assertEq(t, actual, expected, frame, checkFrameRate)
		}
	})
}
