package backend

import (
	"context"
	"fmt"
	"path"
	"path/filepath"
	"reflect"
	"sort"
	"testing"

	"github.com/restic/restic/internal/restic"
	rtest "github.com/restic/restic/internal/test"
)

func TestDefaultLayout(t *testing.T) {
	tempdir, cleanup := rtest.TempDir(t)
	defer cleanup()

	var tests = []struct {
		path string
		join func(...string) string
		restic.Handle
		filename string
	}{
		{
			tempdir,
			filepath.Join,
			restic.Handle{Type: restic.PackFile, Name: "0123456"},
			filepath.Join(tempdir, "data", "01", "0123456"),
		},
		{
			tempdir,
			filepath.Join,
			restic.Handle{Type: restic.ConfigFile, Name: "CFG"},
			filepath.Join(tempdir, "config"),
		},
		{
			tempdir,
			filepath.Join,
			restic.Handle{Type: restic.SnapshotFile, Name: "123456"},
			filepath.Join(tempdir, "snapshots", "123456"),
		},
		{
			tempdir,
			filepath.Join,
			restic.Handle{Type: restic.IndexFile, Name: "123456"},
			filepath.Join(tempdir, "index", "123456"),
		},
		{
			tempdir,
			filepath.Join,
			restic.Handle{Type: restic.LockFile, Name: "123456"},
			filepath.Join(tempdir, "locks", "123456"),
		},
		{
			tempdir,
			filepath.Join,
			restic.Handle{Type: restic.KeyFile, Name: "123456"},
			filepath.Join(tempdir, "keys", "123456"),
		},
		{
			"",
			path.Join,
			restic.Handle{Type: restic.PackFile, Name: "0123456"},
			"data/01/0123456",
		},
		{
			"",
			path.Join,
			restic.Handle{Type: restic.ConfigFile, Name: "CFG"},
			"config",
		},
		{
			"",
			path.Join,
			restic.Handle{Type: restic.SnapshotFile, Name: "123456"},
			"snapshots/123456",
		},
		{
			"",
			path.Join,
			restic.Handle{Type: restic.IndexFile, Name: "123456"},
			"index/123456",
		},
		{
			"",
			path.Join,
			restic.Handle{Type: restic.LockFile, Name: "123456"},
			"locks/123456",
		},
		{
			"",
			path.Join,
			restic.Handle{Type: restic.KeyFile, Name: "123456"},
			"keys/123456",
		},
	}

	t.Run("Paths", func(t *testing.T) {
		l := &DefaultLayout{
			Path: tempdir,
			Join: filepath.Join,
		}

		dirs := l.Paths()

		want := []string{
			filepath.Join(tempdir, "data"),
			filepath.Join(tempdir, "snapshots"),
			filepath.Join(tempdir, "index"),
			filepath.Join(tempdir, "locks"),
			filepath.Join(tempdir, "keys"),
		}

		for i := 0; i < 256; i++ {
			want = append(want, filepath.Join(tempdir, "data", fmt.Sprintf("%02x", i)))
		}

		sort.Strings(want)
		sort.Strings(dirs)

		if !reflect.DeepEqual(dirs, want) {
			t.Fatalf("wrong paths returned, want:\n  %v\ngot:\n  %v", want, dirs)
		}
	})

	for _, test := range tests {
		t.Run(fmt.Sprintf("%v/%v", test.Type, test.Handle.Name), func(t *testing.T) {
			l := &DefaultLayout{
				Path: test.path,
				Join: test.join,
			}

			filename := l.Filename(test.Handle)
			if filename != test.filename {
				t.Fatalf("wrong filename, want %v, got %v", test.filename, filename)
			}
		})
	}
}

func TestRESTLayout(t *testing.T) {
	path, cleanup := rtest.TempDir(t)
	defer cleanup()

	var tests = []struct {
		restic.Handle
		filename string
	}{
		{
			restic.Handle{Type: restic.PackFile, Name: "0123456"},
			filepath.Join(path, "data", "0123456"),
		},
		{
			restic.Handle{Type: restic.ConfigFile, Name: "CFG"},
			filepath.Join(path, "config"),
		},
		{
			restic.Handle{Type: restic.SnapshotFile, Name: "123456"},
			filepath.Join(path, "snapshots", "123456"),
		},
		{
			restic.Handle{Type: restic.IndexFile, Name: "123456"},
			filepath.Join(path, "index", "123456"),
		},
		{
			restic.Handle{Type: restic.LockFile, Name: "123456"},
			filepath.Join(path, "locks", "123456"),
		},
		{
			restic.Handle{Type: restic.KeyFile, Name: "123456"},
			filepath.Join(path, "keys", "123456"),
		},
	}

	l := &RESTLayout{
		Path: path,
		Join: filepath.Join,
	}

	t.Run("Paths", func(t *testing.T) {
		dirs := l.Paths()

		want := []string{
			filepath.Join(path, "data"),
			filepath.Join(path, "snapshots"),
			filepath.Join(path, "index"),
			filepath.Join(path, "locks"),
			filepath.Join(path, "keys"),
		}

		sort.Strings(want)
		sort.Strings(dirs)

		if !reflect.DeepEqual(dirs, want) {
			t.Fatalf("wrong paths returned, want:\n  %v\ngot:\n  %v", want, dirs)
		}
	})

	for _, test := range tests {
		t.Run(fmt.Sprintf("%v/%v", test.Type, test.Handle.Name), func(t *testing.T) {
			filename := l.Filename(test.Handle)
			if filename != test.filename {
				t.Fatalf("wrong filename, want %v, got %v", test.filename, filename)
			}
		})
	}
}

func TestRESTLayoutURLs(t *testing.T) {
	var tests = []struct {
		l   Layout
		h   restic.Handle
		fn  string
		dir string
	}{
		{
			&RESTLayout{URL: "https://hostname.foo", Path: "", Join: path.Join},
			restic.Handle{Type: restic.PackFile, Name: "foobar"},
			"https://hostname.foo/data/foobar",
			"https://hostname.foo/data/",
		},
		{
			&RESTLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join},
			restic.Handle{Type: restic.LockFile, Name: "foobar"},
			"https://hostname.foo:1234/prefix/repo/locks/foobar",
			"https://hostname.foo:1234/prefix/repo/locks/",
		},
		{
			&RESTLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join},
			restic.Handle{Type: restic.ConfigFile, Name: "foobar"},
			"https://hostname.foo:1234/prefix/repo/config",
			"https://hostname.foo:1234/prefix/repo/",
		},
		{
			&S3LegacyLayout{URL: "https://hostname.foo", Path: "/", Join: path.Join},
			restic.Handle{Type: restic.PackFile, Name: "foobar"},
			"https://hostname.foo/data/foobar",
			"https://hostname.foo/data/",
		},
		{
			&S3LegacyLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "", Join: path.Join},
			restic.Handle{Type: restic.LockFile, Name: "foobar"},
			"https://hostname.foo:1234/prefix/repo/lock/foobar",
			"https://hostname.foo:1234/prefix/repo/lock/",
		},
		{
			&S3LegacyLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join},
			restic.Handle{Type: restic.ConfigFile, Name: "foobar"},
			"https://hostname.foo:1234/prefix/repo/config",
			"https://hostname.foo:1234/prefix/repo/",
		},
		{
			&S3LegacyLayout{URL: "", Path: "", Join: path.Join},
			restic.Handle{Type: restic.PackFile, Name: "foobar"},
			"data/foobar",
			"data/",
		},
		{
			&S3LegacyLayout{URL: "", Path: "", Join: path.Join},
			restic.Handle{Type: restic.LockFile, Name: "foobar"},
			"lock/foobar",
			"lock/",
		},
		{
			&S3LegacyLayout{URL: "", Path: "/", Join: path.Join},
			restic.Handle{Type: restic.ConfigFile, Name: "foobar"},
			"/config",
			"/",
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("%T", test.l), func(t *testing.T) {
			fn := test.l.Filename(test.h)
			if fn != test.fn {
				t.Fatalf("wrong filename, want %v, got %v", test.fn, fn)
			}

			dir := test.l.Dirname(test.h)
			if dir != test.dir {
				t.Fatalf("wrong dirname, want %v, got %v", test.dir, dir)
			}
		})
	}
}

func TestS3LegacyLayout(t *testing.T) {
	path, cleanup := rtest.TempDir(t)
	defer cleanup()

	var tests = []struct {
		restic.Handle
		filename string
	}{
		{
			restic.Handle{Type: restic.PackFile, Name: "0123456"},
			filepath.Join(path, "data", "0123456"),
		},
		{
			restic.Handle{Type: restic.ConfigFile, Name: "CFG"},
			filepath.Join(path, "config"),
		},
		{
			restic.Handle{Type: restic.SnapshotFile, Name: "123456"},
			filepath.Join(path, "snapshot", "123456"),
		},
		{
			restic.Handle{Type: restic.IndexFile, Name: "123456"},
			filepath.Join(path, "index", "123456"),
		},
		{
			restic.Handle{Type: restic.LockFile, Name: "123456"},
			filepath.Join(path, "lock", "123456"),
		},
		{
			restic.Handle{Type: restic.KeyFile, Name: "123456"},
			filepath.Join(path, "key", "123456"),
		},
	}

	l := &S3LegacyLayout{
		Path: path,
		Join: filepath.Join,
	}

	t.Run("Paths", func(t *testing.T) {
		dirs := l.Paths()

		want := []string{
			filepath.Join(path, "data"),
			filepath.Join(path, "snapshot"),
			filepath.Join(path, "index"),
			filepath.Join(path, "lock"),
			filepath.Join(path, "key"),
		}

		sort.Strings(want)
		sort.Strings(dirs)

		if !reflect.DeepEqual(dirs, want) {
			t.Fatalf("wrong paths returned, want:\n  %v\ngot:\n  %v", want, dirs)
		}
	})

	for _, test := range tests {
		t.Run(fmt.Sprintf("%v/%v", test.Type, test.Handle.Name), func(t *testing.T) {
			filename := l.Filename(test.Handle)
			if filename != test.filename {
				t.Fatalf("wrong filename, want %v, got %v", test.filename, filename)
			}
		})
	}
}

func TestDetectLayout(t *testing.T) {
	path, cleanup := rtest.TempDir(t)
	defer cleanup()

	var tests = []struct {
		filename string
		want     string
	}{
		{"repo-layout-default.tar.gz", "*backend.DefaultLayout"},
		{"repo-layout-s3legacy.tar.gz", "*backend.S3LegacyLayout"},
	}

	var fs = &LocalFilesystem{}
	for _, test := range tests {
		for _, fs := range []Filesystem{fs, nil} {
			t.Run(fmt.Sprintf("%v/fs-%T", test.filename, fs), func(t *testing.T) {
				rtest.SetupTarTestFixture(t, path, filepath.Join("testdata", test.filename))

				layout, err := DetectLayout(context.TODO(), fs, filepath.Join(path, "repo"))
				if err != nil {
					t.Fatal(err)
				}

				if layout == nil {
					t.Fatal("wanted some layout, but detect returned nil")
				}

				layoutName := fmt.Sprintf("%T", layout)
				if layoutName != test.want {
					t.Fatalf("want layout %v, got %v", test.want, layoutName)
				}

				rtest.RemoveAll(t, filepath.Join(path, "repo"))
			})
		}
	}
}

func TestParseLayout(t *testing.T) {
	path, cleanup := rtest.TempDir(t)
	defer cleanup()

	var tests = []struct {
		layoutName        string
		defaultLayoutName string
		want              string
	}{
		{"default", "", "*backend.DefaultLayout"},
		{"s3legacy", "", "*backend.S3LegacyLayout"},
		{"", "", "*backend.DefaultLayout"},
	}

	rtest.SetupTarTestFixture(t, path, filepath.Join("testdata", "repo-layout-default.tar.gz"))

	for _, test := range tests {
		t.Run(test.layoutName, func(t *testing.T) {
			layout, err := ParseLayout(context.TODO(), &LocalFilesystem{}, test.layoutName, test.defaultLayoutName, filepath.Join(path, "repo"))
			if err != nil {
				t.Fatal(err)
			}

			if layout == nil {
				t.Fatal("wanted some layout, but detect returned nil")
			}

			// test that the functions work (and don't panic)
			_ = layout.Dirname(restic.Handle{Type: restic.PackFile})
			_ = layout.Filename(restic.Handle{Type: restic.PackFile, Name: "1234"})
			_ = layout.Paths()

			layoutName := fmt.Sprintf("%T", layout)
			if layoutName != test.want {
				t.Fatalf("want layout %v, got %v", test.want, layoutName)
			}
		})
	}
}

func TestParseLayoutInvalid(t *testing.T) {
	path, cleanup := rtest.TempDir(t)
	defer cleanup()

	var invalidNames = []string{
		"foo", "bar", "local",
	}

	for _, name := range invalidNames {
		t.Run(name, func(t *testing.T) {
			layout, err := ParseLayout(context.TODO(), nil, name, "", path)
			if err == nil {
				t.Fatalf("expected error not found for layout name %v, layout is %v", name, layout)
			}
		})
	}
}
