// Copyright (c) 2019, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package cache

import (
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"

	"github.com/sylabs/scs-library-client/client"
	"github.com/sylabs/singularity/e2e/internal/e2e"
	"github.com/sylabs/singularity/e2e/internal/testhelper"
	"github.com/sylabs/singularity/internal/pkg/client/cache"
)

type cacheTests struct {
	env e2e.TestEnv
}

const (
	imgName = "alpine_latest.sif"
	imgURL  = "library://alpine:latest"
)

func prepTest(t *testing.T, testEnv e2e.TestEnv, testName string, h *cache.Handle, imagePath string) {
	ensureCacheEmpty(t, testName, imagePath, h)

	testEnv.ImgCacheDir = h.GetBasedir()
	testEnv.RunSingularity(
		t,
		e2e.WithProfile(e2e.UserProfile),
		e2e.WithCommand("pull"),
		e2e.WithArgs([]string{"--force", imagePath, imgURL}...),
		e2e.ExpectExit(0),
	)

	ensureCacheNotEmpty(t, testName, imagePath, h)
}

func (c cacheTests) testNoninteractiveCacheCmds(t *testing.T) {
	tests := []struct {
		name               string
		options            []string
		needImage          bool
		expectedEmptyCache bool
		expectedOutput     string
		exit               int
	}{
		{
			name:               "clean force",
			options:            []string{"clean", "--force"},
			expectedOutput:     "",
			needImage:          true,
			expectedEmptyCache: true,
			exit:               0,
		},
		{
			name:           "clean help",
			options:        []string{"clean", "--help"},
			expectedOutput: "Clean your local Singularity cache",
			needImage:      false,
			exit:           0,
		},
		{
			name:           "list help",
			options:        []string{"list", "--help"},
			expectedOutput: "List your local Singularity cache",
			needImage:      false,
			exit:           0,
		},
		{
			name:               "list type",
			options:            []string{"list", "--type", "library"},
			needImage:          true,
			expectedOutput:     "There are 1 container file",
			expectedEmptyCache: false,
			exit:               0,
		},
		{
			name:               "list verbose",
			needImage:          true,
			options:            []string{"list", "--verbose"},
			expectedOutput:     "NAME",
			expectedEmptyCache: false,
			exit:               0,
		},
	}

	// A directory where we store the image and used by separate commands
	tempDir, imgStoreCleanup := e2e.MakeTempDir(t, "", "", "image store")
	defer imgStoreCleanup(t)
	imagePath := filepath.Join(tempDir, imgName)

	for _, tt := range tests {
		// Each test get its own clean cache directory
		cacheDir, cleanup := e2e.MakeCacheDir(t, "")
		defer cleanup(t)
		h, err := cache.NewHandle(cache.Config{BaseDir: cacheDir})
		if err != nil {
			t.Fatalf("Could not create image cache handle: %v", err)
		}

		if tt.needImage {
			prepTest(t, c.env, tt.name, h, imagePath)
		}

		c.env.ImgCacheDir = cacheDir
		c.env.RunSingularity(
			t,
			e2e.AsSubtest(tt.name),
			e2e.WithProfile(e2e.UserProfile),
			e2e.WithCommand("cache"),
			e2e.WithArgs(tt.options...),
			e2e.ExpectExit(tt.exit),
		)

		if tt.needImage && tt.expectedEmptyCache {
			ensureCacheEmpty(t, tt.name, imagePath, h)
		}
	}
}

func (c cacheTests) testInteractiveCacheCmds(t *testing.T) {
	tt := []struct {
		name               string
		options            []string
		expect             string
		send               string
		exit               int
		expectedEmptyCache bool // Is the cache supposed to be empty after the command is executed
	}{
		{
			name:               "clean normal confirmed",
			options:            []string{"clean"},
			expect:             "Do you want to continue? [N/y]",
			send:               "y",
			expectedEmptyCache: true,
			exit:               0,
		},
		{
			name:               "clean normal not confirmed",
			options:            []string{"clean"},
			expect:             "Do you want to continue? [N/y]",
			send:               "n",
			expectedEmptyCache: false,
			exit:               0,
		},
		{
			name:               "clean normal force",
			options:            []string{"clean", "--force"},
			expectedEmptyCache: true,
			exit:               0,
		},
		{
			name:               "clean dry-run confirmed",
			options:            []string{"clean", "--dry-run"},
			expectedEmptyCache: false,
			exit:               0,
		},
		{
			name:               "clean name confirmed",
			options:            []string{"clean", "--name", imgName},
			expect:             "Do you want to continue? [N/y]",
			send:               "y",
			expectedEmptyCache: true,
			exit:               0,
		},
		{
			name:               "clean name not confirmed",
			options:            []string{"clean", "--name", imgName},
			expect:             "Do you want to continue? [N/y]",
			send:               "n",
			expectedEmptyCache: false,
			exit:               0,
		},
		{
			name:               "clean type confirmed",
			options:            []string{"clean", "--type", "library"},
			expect:             "Do you want to continue? [N/y]",
			send:               "y",
			expectedEmptyCache: true,
			exit:               0,
		},
		{
			name:               "clean type not confirmed",
			options:            []string{"clean", "--type", "library"},
			expect:             "Do you want to continue? [N/y]",
			send:               "n",
			expectedEmptyCache: false,
			exit:               0,
		},
	}

	// A directory where we store the image and used by separate commands
	tempDir, imgStoreCleanup := e2e.MakeTempDir(t, "", "", "image store")
	defer imgStoreCleanup(t)
	imagePath := filepath.Join(tempDir, imgName)

	for _, tc := range tt {
		// Each test get its own clean cache directory
		cacheDir, cleanup := e2e.MakeCacheDir(t, "")
		defer cleanup(t)
		h, err := cache.NewHandle(cache.Config{BaseDir: cacheDir})
		if err != nil {
			t.Fatalf("Could not create image cache handle: %v", err)
		}

		prepTest(t, c.env, tc.name, h, imagePath)

		c.env.ImgCacheDir = cacheDir
		c.env.RunSingularity(
			t,
			e2e.AsSubtest(tc.name),
			e2e.WithProfile(e2e.UserProfile),
			e2e.WithCommand("cache"),
			e2e.WithArgs(tc.options...),
			e2e.ConsoleRun(
				e2e.ConsoleExpect(tc.expect),
				e2e.ConsoleSendLine(tc.send),
			),
			e2e.ExpectExit(tc.exit),
		)

		// Check the content of the cache
		if tc.expectedEmptyCache {
			ensureCacheEmpty(t, tc.name, imagePath, h)
		} else {
			ensureCacheNotEmpty(t, tc.name, imagePath, h)
		}
	}
}

// ensureDirEmpty checks if a directory is empty. If it is not empty, the test fails;
// if the directory does not exist, we return an error to give us a chance to test
// for the expected parent directory (cache clean commands delete different directories
// based on the user's input).
func ensureDirEmpty(t *testing.T, testName string, dir string) error {
	fi, err := ioutil.ReadDir(dir)
	if err != nil {
		if os.IsNotExist(err) {
			return err
		}
		t.Fatalf("Could not read dir %q: %v", dir, err)
	}

	if len(fi) != 0 {
		t.Fatalf("Dir %q is not empty", dir)
	}

	return nil
}

// ensureCacheEmpty checks if the entry related to an image is in the cache or not.
// Cache commands do not necessarily delete the same files/directories based on the options used.
// The best option is to check whether there is an entry in the cache, i.e.,
// <cache_root>/library/<shasum>/<imagename>
func ensureCacheEmpty(t *testing.T, testName string, imagePath string, h *cache.Handle) {
	shasum, err := client.ImageHash(imagePath)
	if err != nil {
		if os.IsNotExist(err) {
			// We may not have the image yet and in that case, we check if the library cache directory is empty
			err := ensureDirEmpty(t, testName, h.Library)
			if err != nil {
				// The library directory of the cache is not there, checking the root (we want to make sure that the cache is still coherent)
				ensureDirEmpty(t, testName, filepath.Join(h.GetBasedir(), "root"))
			}
		} else {
			t.Fatalf("failed to compute shasum for %s: %s", imgName, err)
		}
	}

	path := h.LibraryImage(shasum, imgName)
	if e2e.PathExists(t, path) {
		t.Fatalf("%s failed: %s is still in the cache (%s)", testName, imgName, path)
	}
}

func ensureCacheNotEmpty(t *testing.T, testName string, imagePath string, h *cache.Handle) {
	// Cache commands do not necessarily delete the same files/directories based on the options used.
	// The best option is to check whether there is an entry in the cache, i.e., <cache_root>/library/<shasum>/imagename
	shasum, err := client.ImageHash(imagePath)
	if err != nil {
		t.Fatalf("failed to compute shasum for %s: %s", imagePath, err)
	}

	exists, err := h.LibraryImageExists(shasum, imgName)
	if err != nil {
		t.Fatalf("failed to check if image exists: %s", err)
	}
	if !exists {
		path := h.LibraryImage(shasum, imgName)
		t.Fatalf("failed to pull image; %s does not exist (image is: %s, cache entry is: %s)", imgName, imagePath, path)
	}
}

// E2ETests is the main func to trigger the test suite
func E2ETests(env e2e.TestEnv) func(*testing.T) {
	c := cacheTests{
		env: env,
	}

	return testhelper.TestRunner(map[string]func(*testing.T){
		"interactive commands":     c.testInteractiveCacheCmds,
		"non-interactive commands": c.testNoninteractiveCacheCmds,
	})
}
