// Copyright (C) The Arvados Authors. All rights reserved.
//
// SPDX-License-Identifier: AGPL-3.0

package keepweb

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"git.arvados.org/arvados.git/sdk/go/arvados"
	"git.arvados.org/arvados.git/sdk/go/arvadostest"
	check "gopkg.in/check.v1"
)

func (s *IntegrationSuite) TestCadaverHTTPAuth(c *check.C) {
	s.testCadaver(c, arvadostest.ActiveToken, func(newCollection arvados.Collection) (string, string, string) {
		r := "/c=" + arvadostest.FooAndBarFilesInDirUUID + "/"
		w := "/c=" + newCollection.UUID + "/"
		pdh := "/c=" + strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + "/"
		return r, w, pdh
	}, nil)
}

func (s *IntegrationSuite) TestCadaverPathAuth(c *check.C) {
	s.testCadaver(c, "", func(newCollection arvados.Collection) (string, string, string) {
		r := "/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken + "/"
		w := "/c=" + newCollection.UUID + "/t=" + arvadostest.ActiveToken + "/"
		pdh := "/c=" + strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + "/t=" + arvadostest.ActiveToken + "/"
		return r, w, pdh
	}, nil)
}

func (s *IntegrationSuite) TestCadaverUserProject(c *check.C) {
	rpath := "/users/active/foo_file_in_dir/"
	s.testCadaver(c, arvadostest.ActiveToken, func(newCollection arvados.Collection) (string, string, string) {
		wpath := "/users/active/" + newCollection.Name
		pdh := "/c=" + strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + "/"
		return rpath, wpath, pdh
	}, func(path string) bool {
		// Skip tests that rely on writes, because /users/
		// tree is read-only.
		return !strings.HasPrefix(path, rpath) || strings.HasPrefix(path, rpath+"_/")
	})
}

func (s *IntegrationSuite) testCadaver(c *check.C, password string, pathFunc func(arvados.Collection) (string, string, string), skip func(string) bool) {
	testdata := "the human tragedy consists in the necessity of living with the consequences of actions performed under the pressure of compulsions we do not understand"

	tempdir, err := ioutil.TempDir("", "keep-web-test-")
	c.Assert(err, check.IsNil)
	defer os.RemoveAll(tempdir)

	localfile, err := ioutil.TempFile(tempdir, "localfile")
	c.Assert(err, check.IsNil)
	localfile.Write([]byte(testdata))

	emptyfile, err := ioutil.TempFile(tempdir, "emptyfile")
	c.Assert(err, check.IsNil)

	checkfile, err := ioutil.TempFile(tempdir, "checkfile")
	c.Assert(err, check.IsNil)

	var newCollection arvados.Collection
	arv := arvados.NewClientFromEnv()
	arv.AuthToken = arvadostest.ActiveToken
	err = arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{"collection": map[string]interface{}{}})
	c.Assert(err, check.IsNil)

	readPath, writePath, pdhPath := pathFunc(newCollection)

	matchToday := time.Now().Format("Jan +2")

	type testcase struct {
		path           string
		cmd            string
		match          string
		data           string
		checkemptydata bool
	}
	for _, trial := range []testcase{
		{
			path:  readPath,
			cmd:   "ls\n",
			match: `(?ms).*dir1 *0 .*`,
		},
		{
			path:  readPath,
			cmd:   "ls dir1\n",
			match: `(?ms).*bar *3.*foo *3 .*`,
		},
		{
			path:  readPath + "_/dir1",
			cmd:   "ls\n",
			match: `(?ms).*bar *3.*foo *3 .*`,
		},
		{
			path:  readPath + "dir1/",
			cmd:   "ls\n",
			match: `(?ms).*bar *3.*foo +3 +Feb +\d+ +2014.*`,
		},
		{
			path:  writePath,
			cmd:   "get emptyfile '" + checkfile.Name() + "'\n",
			match: `(?ms).*Not Found.*`,
		},
		{
			path:  writePath,
			cmd:   "put '" + emptyfile.Name() + "' emptyfile\n",
			match: `(?ms).*Uploading .* succeeded.*`,
		},
		{
			path:           writePath,
			cmd:            "get emptyfile '" + checkfile.Name() + "'\n",
			match:          `(?ms).*Downloading .* succeeded.*`,
			checkemptydata: true,
		},
		{
			path:  writePath,
			cmd:   "put '" + localfile.Name() + "' testfile\n",
			match: `(?ms).*Uploading .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "get testfile '" + checkfile.Name() + "'\n",
			match: `(?ms).*succeeded.*`,
			data:  testdata,
		},
		{
			path:  writePath,
			cmd:   "move testfile \"test &#!%20 file\"\n",
			match: `(?ms).*Moving .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "move \"test &#!%20 file\" testfile\n",
			match: `(?ms).*Moving .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "mkcol newdir0/\n",
			match: `(?ms).*Creating .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "move testfile newdir0/\n",
			match: `(?ms).*Moving .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "move testfile newdir0/\n",
			match: `(?ms).*Moving .* failed.*`,
		},
		{
			path:  writePath,
			cmd:   "lock newdir0/testfile\n",
			match: `(?ms).*Locking .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "unlock newdir0/testfile\nasdf\n",
			match: `(?ms).*Unlocking .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "ls\n",
			match: `(?ms).*newdir0.* 0 +` + matchToday + ` \d+:\d+\n.*`,
		},
		{
			path:  writePath,
			cmd:   "move newdir0/testfile emptyfile/bogus/\n",
			match: `(?ms).*Moving .* failed.*`,
		},
		{
			path:  writePath,
			cmd:   "mkcol newdir1\n",
			match: `(?ms).*Creating .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "move newdir1/ newdir1x/\n",
			match: `(?ms).*Moving .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "move newdir1x newdir1\n",
			match: `(?ms).*Moving .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "move newdir0/testfile newdir1/\n",
			match: `(?ms).*Moving .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "move newdir1 newdir1/\n",
			match: `(?ms).*Moving .* failed.*`,
		},
		{
			path:  writePath,
			cmd:   "get newdir1/testfile '" + checkfile.Name() + "'\n",
			match: `(?ms).*succeeded.*`,
			data:  testdata,
		},
		{
			path:  writePath,
			cmd:   "put '" + localfile.Name() + "' newdir1/testfile1\n",
			match: `(?ms).*Uploading .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "mkcol newdir2\n",
			match: `(?ms).*Creating .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "put '" + localfile.Name() + "' newdir2/testfile2\n",
			match: `(?ms).*Uploading .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "copy newdir2/testfile2 testfile3\n",
			match: `(?ms).*succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "get testfile3 '" + checkfile.Name() + "'\n",
			match: `(?ms).*succeeded.*`,
			data:  testdata,
		},
		{
			path:  writePath,
			cmd:   "get newdir2/testfile2 '" + checkfile.Name() + "'\n",
			match: `(?ms).*succeeded.*`,
			data:  testdata,
		},
		{
			path:  writePath,
			cmd:   "rmcol newdir2\n",
			match: `(?ms).*Deleting collection .* succeeded.*`,
		},
		{
			path:  writePath,
			cmd:   "get newdir2/testfile2 '" + checkfile.Name() + "'\n",
			match: `(?ms).*Downloading .* failed.*`,
		},
		{
			path:  "/c=" + arvadostest.UserAgreementCollection + "/t=" + arv.AuthToken + "/",
			cmd:   "put '" + localfile.Name() + "' foo\n",
			match: `(?ms).*Uploading .* failed:.*403 Forbidden.*`,
		},
		{
			path:  pdhPath,
			cmd:   "put '" + localfile.Name() + "' foo\n",
			match: `(?ms).*Uploading .* failed:.*405 Method Not Allowed.*`,
		},
		{
			path:  pdhPath,
			cmd:   "move foo bar\n",
			match: `(?ms).*Moving .* failed:.*405 Method Not Allowed.*`,
		},
		{
			path:  pdhPath,
			cmd:   "copy foo bar\n",
			match: `(?ms).*Copying .* failed:.*405 Method Not Allowed.*`,
		},
		{
			path:  pdhPath,
			cmd:   "delete foo\n",
			match: `(?ms).*Deleting .* failed:.*405 Method Not Allowed.*`,
		},
		{
			path:  pdhPath,
			cmd:   "lock foo\n",
			match: `(?ms).*Locking .* failed:.*405 Method Not Allowed.*`,
		},
	} {
		c.Logf("=== %s trial %+v", s.testServer.URL, trial)
		if skip != nil && skip(trial.path) {
			c.Log("(skip)")
			continue
		}

		os.Remove(checkfile.Name())

		stdout := s.runCadaver(c, password, trial.path, trial.cmd)
		c.Check(stdout, check.Matches, trial.match)

		if trial.data == "" && !trial.checkemptydata {
			continue
		}
		checkfile, err = os.Open(checkfile.Name())
		c.Assert(err, check.IsNil)
		checkfile.Seek(0, os.SEEK_SET)
		got, err := ioutil.ReadAll(checkfile)
		c.Check(string(got), check.Equals, trial.data)
		c.Check(err, check.IsNil)
	}
}

func (s *IntegrationSuite) TestCadaverByID(c *check.C) {
	for _, path := range []string{"/by_id", "/by_id/"} {
		stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
		c.Check(stdout, check.Matches, `(?ms).*collection is empty.*`)
	}
	for _, path := range []string{
		"/by_id/" + arvadostest.FooCollectionPDH,
		"/by_id/" + arvadostest.FooCollectionPDH + "/",
		"/by_id/" + arvadostest.FooCollection,
		"/by_id/" + arvadostest.FooCollection + "/",
	} {
		stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
		c.Check(stdout, check.Matches, `(?ms).*\s+foo\s+3 .*`)
	}
}

func (s *IntegrationSuite) TestCadaverUsersDir(c *check.C) {
	for _, path := range []string{"/"} {
		stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
		c.Check(stdout, check.Matches, `(?ms).*Coll:\s+by_id\s+0 .*`)
		c.Check(stdout, check.Matches, `(?ms).*Coll:\s+users\s+0 .*`)
	}
	for _, path := range []string{"/users", "/users/"} {
		stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
		c.Check(stdout, check.Matches, `(?ms).*Coll:\s+active.*`)
	}
	for _, path := range []string{"/users/active", "/users/active/"} {
		stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
		c.Check(stdout, check.Matches, `(?ms).*Coll:\s+A Project\s+0 .*`)
		c.Check(stdout, check.Matches, `(?ms).*Coll:\s+bar_file\s+0 .*`)
	}
	for _, path := range []string{"/users/admin", "/users/doesnotexist", "/users/doesnotexist/"} {
		stdout := s.runCadaver(c, arvadostest.ActiveToken, path, "ls")
		c.Check(stdout, check.Matches, `(?ms).*404 Not Found.*`)
	}
}

func (s *IntegrationSuite) runCadaver(c *check.C, password, path, stdin string) string {
	tempdir, err := ioutil.TempDir("", "keep-web-test-")
	c.Assert(err, check.IsNil)
	defer os.RemoveAll(tempdir)

	cmd := exec.Command("cadaver", s.testServer.URL+path)
	if password != "" {
		// cadaver won't try username/password authentication
		// unless the server responds 401 to an
		// unauthenticated request, which it only does in
		// AttachmentOnlyHost, TrustAllContent, and
		// per-collection vhost cases.
		s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host = s.testServer.URL[7:]

		cmd.Env = append(os.Environ(), "HOME="+tempdir)
		f, err := os.OpenFile(filepath.Join(tempdir, ".netrc"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
		c.Assert(err, check.IsNil)
		_, err = fmt.Fprintf(f, "default login none password %s\n", password)
		c.Assert(err, check.IsNil)
		c.Assert(f.Close(), check.IsNil)
	}
	cmd.Stdin = bytes.NewBufferString(stdin)
	stdout, err := cmd.StdoutPipe()
	c.Assert(err, check.Equals, nil)
	cmd.Stderr = cmd.Stdout
	go cmd.Start()

	var buf bytes.Buffer
	_, err = io.Copy(&buf, stdout)
	c.Check(err, check.Equals, nil)
	err = cmd.Wait()
	c.Check(err, check.Equals, nil)
	return buf.String()
}
