package compliance

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"math/rand"
	"testing"

	"github.com/gomods/athens/pkg/errors"
	"github.com/gomods/athens/pkg/storage"
	"github.com/stretchr/testify/require"
)

// RunTests takes a backend implementation and runs compliance tests
// against the interface.
func RunTests(t *testing.T, b storage.Backend, clearBackend func() error) {
	require.NoError(t, clearBackend(), "pre-clearing backend failed")
	defer require.NoError(t, clearBackend(), "post-clearing backend failed")
	testNotFound(t, b)
	testList(t, b)
	testListSuffix(t, b)
	testDelete(t, b)
	testGet(t, b)
	testExists(t, b)
	testShouldNotExist(t, b)
}

// testNotFound ensures that a storage Backend
// returns a KindNotFound error when asking for
// non existing modules.
func testNotFound(t *testing.T, b storage.Backend) {
	mod, ver := "github.com/gomods/athens", "yyy"
	ctx := context.Background()

	err := b.Delete(ctx, mod, ver)
	require.Error(t, err)
	require.Equal(t, errors.KindNotFound, errors.Kind(err))

	_, err = b.GoMod(ctx, mod, ver)
	require.Error(t, err)
	require.Equal(t, errors.KindNotFound, errors.Kind(err))

	_, err = b.Info(ctx, mod, ver)
	require.Error(t, err)
	require.Equal(t, errors.KindNotFound, errors.Kind(err))

	vs, err := b.List(ctx, mod)
	require.NoError(t, err)
	require.Equal(t, 0, len(vs))

	_, err = b.Zip(ctx, mod, ver)
	require.Error(t, err)
	require.Equal(t, errors.KindNotFound, errors.Kind(err))
}

// testListPrefixes makes sure that if you have two modules, such as
// github.com/one/two and github.com/one/two-suffix, then the versions
// should not be mixed just because they share a similar prefix.
func testListSuffix(t *testing.T, b storage.Backend) {
	ctx := context.Background()

	modVers := map[string][]string{
		"github.com/one/two":       {"v1.1.0", "v1.2.0", "v1.3.0"},
		"github.com/one/two/v2":    {"v2.1.0"},
		"github.com/one/two-other": {"v0.9.0"},
		"github.com/one":           {}, // not a module but a valid query, so no versions
	}
	for modname, versions := range modVers {
		for _, version := range versions {
			mock := getMockModule()
			err := b.Save(
				ctx,
				modname,
				version,
				mock.Mod,
				mock.Zip,
				mock.Info,
			)
			require.NoError(t, err, "Save for storage failed")
		}
	}
	defer func() {
		for modname, versions := range modVers {
			for _, version := range versions {
				b.Delete(ctx, modname, version)
			}
		}
	}()
	for modname, versions := range modVers {
		retVersions, err := b.List(ctx, modname)
		require.NoError(t, err)
		if len(versions) == 0 {
			require.Empty(t, retVersions)
		} else {
			require.Equal(t, versions, retVersions)
		}
	}
}

// testList tests that a storage Backend returns
// the exact list of versions that are saved.
func testList(t *testing.T, b storage.Backend) {
	ctx := context.Background()

	modname := "github.com/gomods/athens"
	versions := []string{"v1.1.0", "v1.2.0", "v1.3.0"}
	for _, version := range versions {
		mock := getMockModule()
		err := b.Save(
			ctx,
			modname,
			version,
			mock.Mod,
			mock.Zip,
			mock.Info,
		)
		require.NoError(t, err, "Save for storage failed")
	}
	defer func() {
		for _, ver := range versions {
			b.Delete(ctx, modname, ver)
		}
	}()
	retVersions, err := b.List(ctx, modname)
	require.NoError(t, err)
	require.Equal(t, versions, retVersions)
}

// testGet saves and retrieves a module successfully.
func testGet(t *testing.T, b storage.Backend) {
	ctx := context.Background()
	modname := "github.com/gomods/athens"
	ver := "v1.2.3"
	mock := getMockModule()
	zipBts, _ := io.ReadAll(mock.Zip)
	b.Save(ctx, modname, ver, mock.Mod, bytes.NewReader(zipBts), mock.Info)
	defer b.Delete(ctx, modname, ver)

	info, err := b.Info(ctx, modname, ver)
	require.NoError(t, err)
	require.Equal(t, mock.Info, info)

	mod, err := b.GoMod(ctx, modname, ver)
	require.NoError(t, err)
	require.Equal(t, string(mock.Mod), string(mod))

	zip, err := b.Zip(ctx, modname, ver)
	require.NoError(t, err)
	givenZipBts, err := io.ReadAll(zip)
	require.NoError(t, err)
	require.Equal(t, zipBts, givenZipBts)
	require.Equal(t, int64(len(zipBts)), zip.Size())
}

func testExists(t *testing.T, b storage.Backend) {
	ctx := context.Background()
	modname := "github.com/gomods/athens"
	ver := "v1.2.3"
	mock := getMockModule()
	zipBts, _ := io.ReadAll(mock.Zip)
	b.Save(ctx, modname, ver, mock.Mod, bytes.NewReader(zipBts), mock.Info)
	defer b.Delete(ctx, modname, ver)
	checker := storage.WithChecker(b)
	exists, err := checker.Exists(ctx, modname, ver)
	require.NoError(t, err)
	require.Equal(t, true, exists)
}

func testShouldNotExist(t *testing.T, b storage.Backend) {
	ctx := context.Background()
	mod := "github.com/gomods/shouldNotExist"
	ver := "v1.2.3-pre.1"
	mock := getMockModule()
	zipBts, _ := io.ReadAll(mock.Zip)
	err := b.Save(ctx, mod, ver, mock.Mod, bytes.NewReader(zipBts), mock.Info)
	require.NoError(t, err, "should successfully safe a mock module")
	defer b.Delete(ctx, mod, ver)

	prefixVer := "v1.2.3-pre"

	exists, err := storage.WithChecker(b).Exists(ctx, mod, prefixVer)
	require.NoError(t, err)
	if exists {
		t.Fatal("a non existing version that has the same prefix of an existing version should not exist")
	}
}

// testDelete tests that a module can be deleted from a
// storage Backend and the Exists method returns false
// afterwards.
func testDelete(t *testing.T, b storage.Backend) {
	ctx := context.Background()
	modname := "github.com/gomods/athens"
	version := fmt.Sprintf("%s%d", "delete", rand.Int())

	mock := getMockModule()
	err := b.Save(ctx, modname, version, mock.Mod, mock.Zip, mock.Info)
	require.NoError(t, err)

	err = b.Delete(ctx, modname, version)
	require.NoError(t, err)

	exists, err := storage.WithChecker(b).Exists(ctx, modname, version)
	require.NoError(t, err)
	require.Equal(t, false, exists)
}

func getMockModule() *storage.Version {
	return &storage.Version{
		Info: []byte("123"),
		Mod:  []byte("456"),
		Zip:  io.NopCloser(bytes.NewReader([]byte("789"))),
	}
}
