package bimg

import (
	"bytes"
	"crypto/md5"
	"fmt"
	"image"
	"image/jpeg"
	"io/ioutil"
	"os"
	"path"
	"testing"
)

func TestResize(t *testing.T) {
	options := Options{Width: 800, Height: 600}
	buf, _ := Read("testdata/test.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	if DetermineImageType(newImg) != JPEG {
		t.Fatal("Image is not jpeg")
	}

	size, _ := Size(newImg)
	if size.Height != options.Height || size.Width != options.Width {
		t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_out.jpg", newImg)
}

func TestResizeVerticalImage(t *testing.T) {
	tests := []Options{
		{Width: 800, Height: 600},
		{Width: 1000, Height: 1000},
		{Width: 1000, Height: 1500},
		{Width: 1000},
		{Height: 1500},
		{Width: 200, Height: 120},
		{Width: 2000, Height: 2000},
		{Width: 500, Height: 1000},
		{Width: 500},
		{Height: 500},
		{Crop: true, Width: 500, Height: 1000},
		{Crop: true, Enlarge: true, Width: 2000, Height: 1400},
		{Enlarge: true, Force: true, Width: 2000, Height: 2000},
		{Force: true, Width: 2000, Height: 2000},
	}

	bufJpeg, err := Read("testdata/vertical.jpg")
	if err != nil {
		t.Fatal(err)
	}
	bufWebp, err := Read("testdata/vertical.webp")
	if err != nil {
		t.Fatal(err)
	}

	images := []struct {
		format ImageType
		buf    []byte
	}{
		{JPEG, bufJpeg},
		{WEBP, bufWebp},
	}

	for _, source := range images {
		for _, options := range tests {
			image, err := Resize(source.buf, options)
			if err != nil {
				t.Fatalf("Resize(imgData, %#v) error: %#v", options, err)
			}

			format := DetermineImageType(image)
			if format != source.format {
				t.Fatalf("Image format is invalid. Expected: %#v got %v", ImageTypeName(source.format), ImageTypeName(format))
			}

			size, _ := Size(image)
			if options.Height > 0 && size.Height != options.Height {
				t.Fatalf("Invalid height: %d", size.Height)
			}
			if options.Width > 0 && size.Width != options.Width {
				t.Fatalf("Invalid width: %d", size.Width)
			}

			Write(
				fmt.Sprintf(
					"testdata/test_vertical_%dx%d_out.%s",
					options.Width,
					options.Height,
					ImageTypeName(source.format)),
				image)
		}
	}
}

func TestResizeCustomSizes(t *testing.T) {
	tests := []Options{
		{Width: 800, Height: 600},
		{Width: 1000, Height: 1000},
		{Width: 100, Height: 50},
		{Width: 2000, Height: 2000},
		{Width: 500, Height: 1000},
		{Width: 500},
		{Height: 500},
		{Crop: true, Width: 500, Height: 1000},
		{Crop: true, Enlarge: true, Width: 2000, Height: 1400},
		{Enlarge: true, Force: true, Width: 2000, Height: 2000},
		{Force: true, Width: 2000, Height: 2000},
	}

	bufJpeg, err := Read("testdata/test.jpg")
	if err != nil {
		t.Fatal(err)
	}
	bufWebp, err := Read("testdata/test.webp")
	if err != nil {
		t.Fatal(err)
	}

	images := []struct {
		format ImageType
		buf    []byte
	}{
		{JPEG, bufJpeg},
		{WEBP, bufWebp},
	}

	for _, source := range images {
		for _, options := range tests {
			image, err := Resize(source.buf, options)
			if err != nil {
				t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
			}

			if DetermineImageType(image) != source.format {
				t.Fatalf("Image format is invalid. Expected: %#v", source.format)
			}

			size, _ := Size(image)

			invalidHeight := options.Height > 0 && size.Height != options.Height
			if !options.Crop && invalidHeight {
				t.Fatalf("Invalid height: %d, expected %d", size.Height, options.Height)
			}

			invalidWidth := options.Width > 0 && size.Width != options.Width
			if !options.Crop && invalidWidth {
				t.Fatalf("Invalid width: %d, expected %d", size.Width, options.Width)
			}

			if options.Crop && invalidHeight && invalidWidth {
				t.Fatalf("Invalid width or height: %dx%d, expected %dx%d (crop)", size.Width, size.Height, options.Width, options.Height)
			}
		}
	}
}

func TestResizePrecision(t *testing.T) {
	// see https://github.com/h2non/bimg/issues/99
	img := image.NewGray16(image.Rect(0, 0, 1920, 1080))
	input := &bytes.Buffer{}
	jpeg.Encode(input, img, nil)

	opts := Options{Width: 300}
	newImg, err := Resize(input.Bytes(), opts)
	if err != nil {
		t.Fatalf("Resize(imgData, %#v) error: %#v", opts, err)
	}

	size, _ := Size(newImg)
	if size.Width != opts.Width {
		t.Fatalf("Invalid width: %d", size.Width)
	}
}

func TestRotate(t *testing.T) {
	options := Options{Width: 800, Height: 600, Rotate: 270, Crop: true}
	buf, _ := Read("testdata/test.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	if DetermineImageType(newImg) != JPEG {
		t.Error("Image is not jpeg")
	}

	size, _ := Size(newImg)
	if size.Width != options.Width || size.Height != options.Height {
		t.Errorf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_rotate_out.jpg", newImg)
}

func TestInvalidRotateDegrees(t *testing.T) {
	options := Options{Width: 800, Height: 600, Rotate: 111, Crop: true}
	buf, _ := Read("testdata/test.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	if DetermineImageType(newImg) != JPEG {
		t.Errorf("Image is not jpeg")
	}

	size, _ := Size(newImg)
	if size.Width != options.Width || size.Height != options.Height {
		t.Errorf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_rotate_invalid_out.jpg", newImg)
}

func TestCorruptedImage(t *testing.T) {
	options := Options{Width: 800, Height: 600}
	buf, _ := Read("testdata/corrupt.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	if DetermineImageType(newImg) != JPEG {
		t.Fatal("Image is not jpeg")
	}

	size, _ := Size(newImg)
	if size.Height != options.Height || size.Width != options.Width {
		t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_corrupt_out.jpg", newImg)
}

func TestNoColorProfile(t *testing.T) {
	options := Options{Width: 800, Height: 600, NoProfile: true}
	buf, _ := Read("testdata/test.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	metadata, err := Metadata(newImg)
	if metadata.Profile == true {
		t.Fatal("Invalid profile data")
	}

	size, _ := Size(newImg)
	if size.Height != options.Height || size.Width != options.Width {
		t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
	}
}

func TestEmbedExtendColor(t *testing.T) {
	options := Options{Width: 400, Height: 600, Crop: false, Embed: true, Extend: ExtendWhite, Background: Color{255, 20, 10}}
	buf, _ := Read("testdata/test_issue.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	size, _ := Size(newImg)
	if size.Height != options.Height || size.Width != options.Width {
		t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_extend_white_out.jpg", newImg)
}

func TestEmbedExtendWithCustomColor(t *testing.T) {
	options := Options{Width: 400, Height: 600, Crop: false, Embed: true, Extend: 5, Background: Color{255, 20, 10}}
	buf, _ := Read("testdata/test_issue.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	size, _ := Size(newImg)
	if size.Height != options.Height || size.Width != options.Width {
		t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_extend_background_out.jpg", newImg)
}

func TestGaussianBlur(t *testing.T) {
	options := Options{Width: 800, Height: 600, GaussianBlur: GaussianBlur{Sigma: 5}}
	buf, _ := Read("testdata/test.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	size, _ := Size(newImg)
	if size.Height != options.Height || size.Width != options.Width {
		t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_gaussian_out.jpg", newImg)
}

func TestSharpen(t *testing.T) {
	options := Options{Width: 800, Height: 600, Sharpen: Sharpen{Radius: 1, X1: 1.5, Y2: 20, Y3: 50, M1: 1, M2: 2}}
	buf, _ := Read("testdata/test.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	size, _ := Size(newImg)
	if size.Height != options.Height || size.Width != options.Width {
		t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_sharpen_out.jpg", newImg)
}

func TestExtractWithDefaultAxis(t *testing.T) {
	options := Options{AreaWidth: 200, AreaHeight: 200}
	buf, _ := Read("testdata/test.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	size, _ := Size(newImg)
	if size.Height != options.AreaHeight || size.Width != options.AreaWidth {
		t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_extract_defaults_out.jpg", newImg)
}

func TestExtractCustomAxis(t *testing.T) {
	options := Options{Top: 100, Left: 100, AreaWidth: 200, AreaHeight: 200}
	buf, _ := Read("testdata/test.jpg")

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	size, _ := Size(newImg)
	if size.Height != options.AreaHeight || size.Width != options.AreaWidth {
		t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
	}

	Write("testdata/test_extract_custom_axis_out.jpg", newImg)
}

func TestExtractOrEmbedImage(t *testing.T) {
	buf, _ := Read("testdata/test.jpg")
	input, _, err := loadImage(buf)
	if err != nil {
		t.Fatalf("Unable to load image %s", err)
	}

	o := Options{
		Top:    10,
		Left:   10,
		Width:  100,
		Height: 200,

		// Fields to test
		AreaHeight: 0,
		AreaWidth:  0,

		Quality: 100, /* Needs a value to satisfy libvips */
	}

	result, err := extractOrEmbedImage(input, o)
	if err != nil {
		if err == ErrExtractAreaParamsRequired {
			t.Fatalf("Expecting AreaWidth and AreaHeight to have been defined")
		}

		t.Fatalf("Unknown error occurred %s", err)
	}

	image, err := saveImage(result, o)
	if err != nil {
		t.Fatalf("Failed saving image %s", err)
	}

	test, err := Size(image)
	if err != nil {
		t.Fatalf("Failed fetching the size %s", err)
	}

	if test.Height != o.Height {
		t.Errorf("Extract failed, resulting Height %d doesn't match %d", test.Height, o.Height)
	}

	if test.Width != o.Width {
		t.Errorf("Extract failed, resulting Width %d doesn't match %d", test.Width, o.Width)
	}
}

func TestConvert(t *testing.T) {
	width, height := 300, 240
	formats := [3]ImageType{PNG, WEBP, JPEG}

	files := []string{
		"test.jpg",
		"test.png",
		"test.webp",
	}

	for _, file := range files {
		img, err := os.Open("testdata/" + file)
		if err != nil {
			t.Fatal(err)
		}

		buf, err := ioutil.ReadAll(img)
		if err != nil {
			t.Fatal(err)
		}
		img.Close()

		for _, format := range formats {
			options := Options{Width: width, Height: height, Crop: true, Type: format}

			newImg, err := Resize(buf, options)
			if err != nil {
				t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
			}

			if DetermineImageType(newImg) != format {
				t.Fatal("Image is not png")
			}

			size, _ := Size(newImg)
			if size.Height != height || size.Width != width {
				t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
			}
		}
	}
}

func TestResizePngWithTransparency(t *testing.T) {
	width, height := 300, 240

	options := Options{Width: width, Height: height, Crop: true}
	img, err := os.Open("testdata/transparent.png")
	if err != nil {
		t.Fatal(err)
	}
	defer img.Close()

	buf, err := ioutil.ReadAll(img)
	if err != nil {
		t.Fatal(err)
	}

	newImg, err := Resize(buf, options)
	if err != nil {
		t.Errorf("Resize(imgData, %#v) error: %#v", options, err)
	}

	if DetermineImageType(newImg) != PNG {
		t.Fatal("Image is not png")
	}

	size, _ := Size(newImg)
	if size.Height != height || size.Width != width {
		t.Fatal("Invalid image size")
	}

	Write("testdata/transparent_out.png", newImg)
}

func TestRotationAndFlip(t *testing.T) {
	files := []struct {
		Name  string
		Angle Angle
		Flip  bool
	}{
		{"Landscape_1", 0, false},
		{"Landscape_2", 0, true},
		{"Landscape_3", D180, false},
		{"Landscape_4", D180, true},
		{"Landscape_5", D90, true},
		{"Landscape_6", D90, false},
		{"Landscape_7", D270, true},
		{"Landscape_8", D270, false},
		{"Portrait_1", 0, false},
		{"Portrait_2", 0, true},
		{"Portrait_3", D180, false},
		{"Portrait_4", D180, true},
		{"Portrait_5", D90, true},
		{"Portrait_6", D90, false},
		{"Portrait_7", D270, true},
		{"Portrait_8", D270, false},
	}

	for _, file := range files {
		img, err := os.Open(fmt.Sprintf("testdata/exif/%s.jpg", file.Name))
		if err != nil {
			t.Fatal(err)
		}

		buf, err := ioutil.ReadAll(img)
		if err != nil {
			t.Fatal(err)
		}
		img.Close()

		image, _, err := loadImage(buf)
		if err != nil {
			t.Fatal(err)
		}

		angle, flip := calculateRotationAndFlip(image, D0)
		if angle != file.Angle {
			t.Errorf("Rotation for %v expected to be %v. got %v", file.Name, file.Angle, angle)
		}
		if flip != file.Flip {
			t.Errorf("Flip for %v expected to be %v. got %v", file.Name, file.Flip, flip)
		}

		// Visual debugging.
		newImg, err := Resize(buf, Options{})
		if err != nil {
			t.Fatal(err)
		}

		Write(fmt.Sprintf("testdata/exif/%s_out.jpg", file.Name), newImg)
	}
}

func TestIfBothSmartCropOptionsAreIdentical(t *testing.T) {
	if !(VipsMajorVersion >= 8 && VipsMinorVersion > 4) {
		t.Skipf("Skipping this test, libvips doesn't meet version requirement %s > 8.4", VipsVersion)
	}

	benchmarkOptions := Options{Width: 100, Height: 100, Crop: true}
	smartCropOptions := Options{Width: 100, Height: 100, Crop: true, SmartCrop: true}
	gravityOptions := Options{Width: 100, Height: 100, Crop: true, Gravity: GravitySmart}

	testImg, err := os.Open("testdata/northern_cardinal_bird.jpg")
	if err != nil {
		t.Fatal(err)
	}
	defer testImg.Close()

	testImgByte, err := ioutil.ReadAll(testImg)
	if err != nil {
		t.Fatal(err)
	}

	scImg, err := Resize(testImgByte, smartCropOptions)
	if err != nil {
		t.Fatal(err)
	}

	gImg, err := Resize(testImgByte, gravityOptions)
	if err != nil {
		t.Fatal(err)
	}

	benchmarkImg, err := Resize(testImgByte, benchmarkOptions)
	if err != nil {
		t.Fatal(err)
	}

	sch, gh, bh := md5.Sum(scImg), md5.Sum(gImg), md5.Sum(benchmarkImg)
	if gh == bh || sch == bh {
		t.Error("Expected both options produce a different result from a standard crop.")
	}

	if sch != gh {
		t.Errorf("Expected both options to result in the same output, %x != %x", sch, gh)
	}
}

func TestSkipCropIfTooSmall(t *testing.T) {
	testCases := []struct {
		name    string
		options Options
	}{
		{
			name: "smart crop",
			options: Options{
				Width:   140,
				Height:  140,
				Crop:    true,
				Gravity: GravitySmart,
			},
		},
		{
			name: "centre crop",
			options: Options{
				Width:   140,
				Height:  140,
				Crop:    true,
				Gravity: GravityCentre,
			},
		},
		{
			name: "embed",
			options: Options{
				Width:  140,
				Height: 140,
				Embed:  true,
			},
		},
		{
			name: "extract",
			options: Options{
				Top:        0,
				Left:       0,
				AreaWidth:  140,
				AreaHeight: 140,
			},
		},
	}

	testImg, err := os.Open("testdata/test_bad_extract_area.jpg")
	if err != nil {
		t.Fatal(err)
	}
	defer testImg.Close()

	testImgByte, err := ioutil.ReadAll(testImg)
	if err != nil {
		t.Fatal(err)
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {

			croppedImage, err := Resize(testImgByte, tc.options)
			if err != nil {
				t.Fatal(err)
			}

			size, _ := Size(croppedImage)
			if tc.options.Height-size.Height > 1 || tc.options.Width-size.Width > 1 {
				t.Fatalf("Invalid image size: %dx%d", size.Width, size.Height)
			}
			t.Logf("size for %s is %dx%d", tc.name, size.Width, size.Height)
		})
	}
}

func runBenchmarkResize(file string, o Options, b *testing.B) {
	buf, _ := Read(path.Join("testdata", file))

	for n := 0; n < b.N; n++ {
		Resize(buf, o)
	}
}

func BenchmarkRotateJpeg(b *testing.B) {
	options := Options{Rotate: 180}
	runBenchmarkResize("test.jpg", options, b)
}

func BenchmarkResizeLargeJpeg(b *testing.B) {
	options := Options{
		Width:  800,
		Height: 600,
	}
	runBenchmarkResize("test.jpg", options, b)
}

func BenchmarkResizePng(b *testing.B) {
	options := Options{
		Width:  200,
		Height: 200,
	}
	runBenchmarkResize("test.png", options, b)
}

func BenchmarkResizeWebp(b *testing.B) {
	options := Options{
		Width:  200,
		Height: 200,
	}
	runBenchmarkResize("test.webp", options, b)
}

func BenchmarkConvertToJpeg(b *testing.B) {
	options := Options{Type: JPEG}
	runBenchmarkResize("test.png", options, b)
}

func BenchmarkConvertToPng(b *testing.B) {
	options := Options{Type: PNG}
	runBenchmarkResize("test.jpg", options, b)
}

func BenchmarkConvertToWebp(b *testing.B) {
	options := Options{Type: WEBP}
	runBenchmarkResize("test.jpg", options, b)
}

func BenchmarkCropJpeg(b *testing.B) {
	options := Options{
		Width:  800,
		Height: 600,
	}
	runBenchmarkResize("test.jpg", options, b)
}

func BenchmarkCropPng(b *testing.B) {
	options := Options{
		Width:  800,
		Height: 600,
	}
	runBenchmarkResize("test.png", options, b)
}

func BenchmarkCropWebp(b *testing.B) {
	options := Options{
		Width:  800,
		Height: 600,
	}
	runBenchmarkResize("test.webp", options, b)
}

func BenchmarkExtractJpeg(b *testing.B) {
	options := Options{
		Top:        100,
		Left:       50,
		AreaWidth:  600,
		AreaHeight: 480,
	}
	runBenchmarkResize("test.jpg", options, b)
}

func BenchmarkExtractPng(b *testing.B) {
	options := Options{
		Top:        100,
		Left:       50,
		AreaWidth:  600,
		AreaHeight: 480,
	}
	runBenchmarkResize("test.png", options, b)
}

func BenchmarkExtractWebp(b *testing.B) {
	options := Options{
		Top:        100,
		Left:       50,
		AreaWidth:  600,
		AreaHeight: 480,
	}
	runBenchmarkResize("test.webp", options, b)
}

func BenchmarkZoomJpeg(b *testing.B) {
	options := Options{Zoom: 1}
	runBenchmarkResize("test.jpg", options, b)
}

func BenchmarkZoomPng(b *testing.B) {
	options := Options{Zoom: 1}
	runBenchmarkResize("test.png", options, b)
}

func BenchmarkZoomWebp(b *testing.B) {
	options := Options{Zoom: 1}
	runBenchmarkResize("test.webp", options, b)
}

func BenchmarkWatermarkJpeg(b *testing.B) {
	options := Options{
		Watermark: Watermark{
			Text:       "Chuck Norris (c) 2315",
			Opacity:    0.25,
			Width:      200,
			DPI:        100,
			Margin:     150,
			Font:       "sans bold 12",
			Background: Color{255, 255, 255},
		},
	}
	runBenchmarkResize("test.jpg", options, b)
}

func BenchmarkWatermarkPng(b *testing.B) {
	options := Options{
		Watermark: Watermark{
			Text:       "Chuck Norris (c) 2315",
			Opacity:    0.25,
			Width:      200,
			DPI:        100,
			Margin:     150,
			Font:       "sans bold 12",
			Background: Color{255, 255, 255},
		},
	}
	runBenchmarkResize("test.png", options, b)
}

func BenchmarkWatermarkWebp(b *testing.B) {
	options := Options{
		Watermark: Watermark{
			Text:       "Chuck Norris (c) 2315",
			Opacity:    0.25,
			Width:      200,
			DPI:        100,
			Margin:     150,
			Font:       "sans bold 12",
			Background: Color{255, 255, 255},
		},
	}
	runBenchmarkResize("test.webp", options, b)
}

func BenchmarkWatermarkImageJpeg(b *testing.B) {
	watermark := readFile("transparent.png")
	options := Options{
		WatermarkImage: WatermarkImage{
			Buf:     watermark,
			Opacity: 0.25,
			Left:    100,
			Top:     100,
		},
	}
	runBenchmarkResize("test.jpg", options, b)
}

func BenchmarkWatermarkImagePng(b *testing.B) {
	watermark := readFile("transparent.png")
	options := Options{
		WatermarkImage: WatermarkImage{
			Buf:     watermark,
			Opacity: 0.25,
			Left:    100,
			Top:     100,
		},
	}
	runBenchmarkResize("test.png", options, b)
}

func BenchmarkWatermarkImageWebp(b *testing.B) {
	watermark := readFile("transparent.png")
	options := Options{
		WatermarkImage: WatermarkImage{
			Buf:     watermark,
			Opacity: 0.25,
			Left:    100,
			Top:     100,
		},
	}
	runBenchmarkResize("test.webp", options, b)
}
