// Copyright 2017 Vector Creations Ltd
//
// 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 main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"time"

	"github.com/matrix-org/dendrite/common/test"
	"github.com/matrix-org/gomatrixserverlib"
	"gopkg.in/yaml.v2"
)

var (
	// How long to wait for the server to write the expected output messages.
	// This needs to be high enough to account for the time it takes to create
	// the postgres database tables which can take a while on travis.
	timeoutString = test.Defaulting(os.Getenv("TIMEOUT"), "10s")
	// The name of maintenance database to connect to in order to create the test database.
	postgresDatabase = test.Defaulting(os.Getenv("POSTGRES_DATABASE"), "postgres")
	// The name of the test database to create.
	testDatabaseName = test.Defaulting(os.Getenv("DATABASE_NAME"), "mediaapi_test")
	// Postgres docker container name (for running psql). If not set, psql must be in PATH.
	postgresContainerName = os.Getenv("POSTGRES_CONTAINER")
	// Test image to be uploaded/downloaded
	testJPEG = test.Defaulting(os.Getenv("TEST_JPEG_PATH"), "src/github.com/matrix-org/dendrite/cmd/mediaapi-integration-tests/totem.jpg")
	kafkaURI = test.Defaulting(os.Getenv("KAFKA_URIS"), "localhost:9092")
)

var thumbnailSizes = (`
- width: 32
  height: 32
  method: crop
- width: 96
  height: 96
  method: crop
- width: 320
  height: 240
  method: scale
- width: 640
  height: 480
  method: scale
- width: 800
  height: 600
  method: scale
`)

const serverType = "media-api"

const testMediaID = "1VuVy8u_hmDllD8BrcY0deM34Bl7SPJeY9J6BkMmpx0"
const testContentType = "image/jpeg"
const testOrigin = "localhost:18001"

var testDatabaseTemplate = "dbname=%s sslmode=disable binary_parameters=yes"

var timeout time.Duration

var port = 10000

func startMediaAPI(suffix string, dynamicThumbnails bool) (*exec.Cmd, chan error, string, *exec.Cmd, chan error, string, string) {
	dir, err := ioutil.TempDir("", serverType+"-server-test"+suffix)
	if err != nil {
		panic(err)
	}

	proxyAddr := "localhost:1800" + suffix

	database := fmt.Sprintf(testDatabaseTemplate, testDatabaseName+suffix)
	cfg, nextPort, err := test.MakeConfig(dir, kafkaURI, database, "localhost", port)
	if err != nil {
		panic(err)
	}
	cfg.Matrix.ServerName = gomatrixserverlib.ServerName(proxyAddr)
	cfg.Media.DynamicThumbnails = dynamicThumbnails
	if err = yaml.Unmarshal([]byte(thumbnailSizes), &cfg.Media.ThumbnailSizes); err != nil {
		panic(err)
	}

	port = nextPort
	if err = test.WriteConfig(cfg, dir); err != nil {
		panic(err)
	}

	serverArgs := []string{
		"--config", filepath.Join(dir, test.ConfigFile),
	}

	databases := []string{
		testDatabaseName + suffix,
	}

	proxyCmd, proxyCmdChan := test.StartProxy(proxyAddr, cfg)

	test.InitDatabase(
		postgresDatabase,
		postgresContainerName,
		databases,
	)

	cmd, cmdChan := test.CreateBackgroundCommand(
		filepath.Join(filepath.Dir(os.Args[0]), "dendrite-"+serverType+"-server"),
		serverArgs,
	)

	fmt.Printf("==TESTSERVER== STARTED %v -> %v : %v\n", proxyAddr, cfg.Listen.MediaAPI, dir)
	return cmd, cmdChan, string(cfg.Listen.MediaAPI), proxyCmd, proxyCmdChan, proxyAddr, dir
}

func cleanUpServer(cmd *exec.Cmd, dir string) {
	// ensure server is dead, only cleaning up so don't care about errors this returns
	cmd.Process.Kill() // nolint: errcheck
	if err := os.RemoveAll(dir); err != nil {
		fmt.Printf("WARNING: Failed to remove temporary directory %v: %q\n", dir, err)
	}
}

// Runs a battery of media API server tests
// The tests will pause at various points in this list to conduct tests on the HTTP responses before continuing.
func main() {
	fmt.Println("==TESTING==", os.Args[0])

	var err error
	timeout, err = time.ParseDuration(timeoutString)
	if err != nil {
		fmt.Printf("ERROR: Invalid timeout string %v: %q\n", timeoutString, err)
		return
	}

	// create server1 with only pre-generated thumbnails allowed
	server1Cmd, server1CmdChan, _, server1ProxyCmd, _, server1ProxyAddr, server1Dir := startMediaAPI("1", false)
	defer cleanUpServer(server1Cmd, server1Dir)
	defer server1ProxyCmd.Process.Kill() // nolint: errcheck
	testDownload(server1ProxyAddr, server1ProxyAddr, "doesnotexist", 404, server1CmdChan)

	// upload a JPEG file
	testUpload(
		server1ProxyAddr, testJPEG,
	)

	// download that JPEG file
	testDownload(server1ProxyAddr, testOrigin, testMediaID, 200, server1CmdChan)

	// thumbnail that JPEG file
	testThumbnail(64, 64, "crop", server1ProxyAddr, server1CmdChan)

	// create server2 with dynamic thumbnail generation
	server2Cmd, server2CmdChan, _, server2ProxyCmd, _, server2ProxyAddr, server2Dir := startMediaAPI("2", true)
	defer cleanUpServer(server2Cmd, server2Dir)
	defer server2ProxyCmd.Process.Kill() // nolint: errcheck
	testDownload(server2ProxyAddr, server2ProxyAddr, "doesnotexist", 404, server2CmdChan)

	// pre-generated thumbnail that JPEG file via server2
	testThumbnail(800, 600, "scale", server2ProxyAddr, server2CmdChan)

	// download that JPEG file via server2
	testDownload(server2ProxyAddr, testOrigin, testMediaID, 200, server2CmdChan)

	// dynamic thumbnail that JPEG file via server2
	testThumbnail(1920, 1080, "scale", server2ProxyAddr, server2CmdChan)

	// thumbnail that JPEG file via server2
	testThumbnail(10000, 10000, "scale", server2ProxyAddr, server2CmdChan)

}

func getMediaURI(host, endpoint, query string, components []string) string {
	pathComponents := []string{host, "_matrix/media/v1", endpoint}
	pathComponents = append(pathComponents, components...)
	return "https://" + path.Join(pathComponents...) + query
}

func testUpload(host, filePath string) {
	fmt.Printf("==TESTING== upload %v to %v\n", filePath, host)
	file, err := os.Open(filePath)
	defer file.Close() // nolint: errcheck, staticcheck, megacheck
	if err != nil {
		panic(err)
	}
	filename := filepath.Base(filePath)
	stat, err := file.Stat()
	if os.IsNotExist(err) {
		panic(err)
	}
	fileSize := stat.Size()

	req, err := http.NewRequest(
		"POST",
		getMediaURI(host, "upload", "?filename="+filename, nil),
		file,
	)
	if err != nil {
		panic(err)
	}
	req.ContentLength = fileSize
	req.Header.Set("Content-Type", testContentType)

	wantedBody := `{"content_uri": "mxc://localhost:18001/` + testMediaID + `"}`
	testReq := &test.Request{
		Req:              req,
		WantedStatusCode: 200,
		WantedBody:       test.CanonicalJSONInput([]string{wantedBody})[0],
	}
	if err := testReq.Do(); err != nil {
		panic(err)
	}
	fmt.Printf("==TESTING== upload %v to %v PASSED\n", filePath, host)
}

func testDownload(host, origin, mediaID string, wantedStatusCode int, serverCmdChan chan error) {
	req, err := http.NewRequest(
		"GET",
		getMediaURI(host, "download", "", []string{
			origin,
			mediaID,
		}),
		nil,
	)
	if err != nil {
		panic(err)
	}
	testReq := &test.Request{
		Req:              req,
		WantedStatusCode: wantedStatusCode,
		WantedBody:       test.CanonicalJSONInput([]string{""})[0],
	}
	testReq.Run(fmt.Sprintf("download mxc://%v/%v from %v", origin, mediaID, host), timeout, serverCmdChan)
}

func testThumbnail(width, height int, resizeMethod, host string, serverCmdChan chan error) {
	query := fmt.Sprintf("?width=%v&height=%v", width, height)
	if resizeMethod != "" {
		query += "&method=" + resizeMethod
	}
	req, err := http.NewRequest(
		"GET",
		getMediaURI(host, "thumbnail", query, []string{
			testOrigin,
			testMediaID,
		}),
		nil,
	)
	if err != nil {
		panic(err)
	}
	testReq := &test.Request{
		Req:              req,
		WantedStatusCode: 200,
		WantedBody:       test.CanonicalJSONInput([]string{""})[0],
	}
	testReq.Run(fmt.Sprintf("thumbnail mxc://%v/%v%v from %v", testOrigin, testMediaID, query, host), timeout, serverCmdChan)
}
