package image

import (
	"bytes"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"

	"github.com/docker/distribution/digest"
)

func TestFSGetSet(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "images-fs-store")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpdir)
	fs, err := NewFSStoreBackend(tmpdir)
	if err != nil {
		t.Fatal(err)
	}

	testGetSet(t, fs)
}

func TestFSGetInvalidData(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "images-fs-store")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpdir)
	fs, err := NewFSStoreBackend(tmpdir)
	if err != nil {
		t.Fatal(err)
	}

	id, err := fs.Set([]byte("foobar"))
	if err != nil {
		t.Fatal(err)
	}

	dgst := digest.Digest(id)

	if err := ioutil.WriteFile(filepath.Join(tmpdir, contentDirName, string(dgst.Algorithm()), dgst.Hex()), []byte("foobar2"), 0600); err != nil {
		t.Fatal(err)
	}

	_, err = fs.Get(id)
	if err == nil {
		t.Fatal("Expected get to fail after data modification.")
	}
}

func TestFSInvalidSet(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "images-fs-store")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpdir)
	fs, err := NewFSStoreBackend(tmpdir)
	if err != nil {
		t.Fatal(err)
	}

	id := digest.FromBytes([]byte("foobar"))
	err = os.Mkdir(filepath.Join(tmpdir, contentDirName, string(id.Algorithm()), id.Hex()), 0700)
	if err != nil {
		t.Fatal(err)
	}

	_, err = fs.Set([]byte("foobar"))
	if err == nil {
		t.Fatal("Expecting error from invalid filesystem data.")
	}
}

func TestFSInvalidRoot(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "images-fs-store")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpdir)

	tcases := []struct {
		root, invalidFile string
	}{
		{"root", "root"},
		{"root", "root/content"},
		{"root", "root/metadata"},
	}

	for _, tc := range tcases {
		root := filepath.Join(tmpdir, tc.root)
		filePath := filepath.Join(tmpdir, tc.invalidFile)
		err := os.MkdirAll(filepath.Dir(filePath), 0700)
		if err != nil {
			t.Fatal(err)
		}
		f, err := os.Create(filePath)
		if err != nil {
			t.Fatal(err)
		}
		f.Close()

		_, err = NewFSStoreBackend(root)
		if err == nil {
			t.Fatalf("Expected error from root %q and invlid file %q", tc.root, tc.invalidFile)
		}

		os.RemoveAll(root)
	}

}

func testMetadataGetSet(t *testing.T, store StoreBackend) {
	id, err := store.Set([]byte("foo"))
	if err != nil {
		t.Fatal(err)
	}
	id2, err := store.Set([]byte("bar"))
	if err != nil {
		t.Fatal(err)
	}

	tcases := []struct {
		id    digest.Digest
		key   string
		value []byte
	}{
		{id, "tkey", []byte("tval1")},
		{id, "tkey2", []byte("tval2")},
		{id2, "tkey", []byte("tval3")},
	}

	for _, tc := range tcases {
		err = store.SetMetadata(tc.id, tc.key, tc.value)
		if err != nil {
			t.Fatal(err)
		}

		actual, err := store.GetMetadata(tc.id, tc.key)
		if err != nil {
			t.Fatal(err)
		}
		if bytes.Compare(actual, tc.value) != 0 {
			t.Fatalf("Metadata expected %q, got %q", tc.value, actual)
		}
	}

	_, err = store.GetMetadata(id2, "tkey2")
	if err == nil {
		t.Fatal("Expected error for getting metadata for unknown key")
	}

	id3 := digest.FromBytes([]byte("baz"))
	err = store.SetMetadata(id3, "tkey", []byte("tval"))
	if err == nil {
		t.Fatal("Expected error for setting metadata for unknown ID.")
	}

	_, err = store.GetMetadata(id3, "tkey")
	if err == nil {
		t.Fatal("Expected error for getting metadata for unknown ID.")
	}
}

func TestFSMetadataGetSet(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "images-fs-store")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpdir)
	fs, err := NewFSStoreBackend(tmpdir)
	if err != nil {
		t.Fatal(err)
	}

	testMetadataGetSet(t, fs)
}

func TestFSDelete(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "images-fs-store")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpdir)
	fs, err := NewFSStoreBackend(tmpdir)
	if err != nil {
		t.Fatal(err)
	}

	testDelete(t, fs)
}

func TestFSWalker(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "images-fs-store")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpdir)
	fs, err := NewFSStoreBackend(tmpdir)
	if err != nil {
		t.Fatal(err)
	}

	testWalker(t, fs)
}

func TestFSInvalidWalker(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "images-fs-store")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpdir)
	fs, err := NewFSStoreBackend(tmpdir)
	if err != nil {
		t.Fatal(err)
	}

	fooID, err := fs.Set([]byte("foo"))
	if err != nil {
		t.Fatal(err)
	}

	if err := ioutil.WriteFile(filepath.Join(tmpdir, contentDirName, "sha256/foobar"), []byte("foobar"), 0600); err != nil {
		t.Fatal(err)
	}

	n := 0
	err = fs.Walk(func(id digest.Digest) error {
		if id != fooID {
			t.Fatalf("Invalid walker ID %q, expected %q", id, fooID)
		}
		n++
		return nil
	})
	if err != nil {
		t.Fatalf("Invalid data should not have caused walker error, got %v", err)
	}
	if n != 1 {
		t.Fatalf("Expected 1 walk initialization, got %d", n)
	}
}

func testGetSet(t *testing.T, store StoreBackend) {
	type tcase struct {
		input    []byte
		expected digest.Digest
	}
	tcases := []tcase{
		{[]byte("foobar"), digest.Digest("sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")},
	}

	randomInput := make([]byte, 8*1024)
	_, err := rand.Read(randomInput)
	if err != nil {
		t.Fatal(err)
	}
	// skipping use of digest pkg because its used by the implementation
	h := sha256.New()
	_, err = h.Write(randomInput)
	if err != nil {
		t.Fatal(err)
	}
	tcases = append(tcases, tcase{
		input:    randomInput,
		expected: digest.Digest("sha256:" + hex.EncodeToString(h.Sum(nil))),
	})

	for _, tc := range tcases {
		id, err := store.Set([]byte(tc.input))
		if err != nil {
			t.Fatal(err)
		}
		if id != tc.expected {
			t.Fatalf("Expected ID %q, got %q", tc.expected, id)
		}
	}

	for _, emptyData := range [][]byte{nil, {}} {
		_, err := store.Set(emptyData)
		if err == nil {
			t.Fatal("Expected error for nil input.")
		}
	}

	for _, tc := range tcases {
		data, err := store.Get(tc.expected)
		if err != nil {
			t.Fatal(err)
		}
		if bytes.Compare(data, tc.input) != 0 {
			t.Fatalf("Expected data %q, got %q", tc.input, data)
		}
	}

	for _, key := range []digest.Digest{"foobar:abc", "sha256:abc", "sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2a"} {
		_, err := store.Get(key)
		if err == nil {
			t.Fatalf("Expected error for ID %q.", key)
		}
	}

}

func testDelete(t *testing.T, store StoreBackend) {
	id, err := store.Set([]byte("foo"))
	if err != nil {
		t.Fatal(err)
	}
	id2, err := store.Set([]byte("bar"))
	if err != nil {
		t.Fatal(err)
	}

	err = store.Delete(id)
	if err != nil {
		t.Fatal(err)
	}

	_, err = store.Get(id)
	if err == nil {
		t.Fatalf("Expected getting deleted item %q to fail", id)
	}
	_, err = store.Get(id2)
	if err != nil {
		t.Fatal(err)
	}

	err = store.Delete(id2)
	if err != nil {
		t.Fatal(err)
	}
	_, err = store.Get(id2)
	if err == nil {
		t.Fatalf("Expected getting deleted item %q to fail", id2)
	}
}

func testWalker(t *testing.T, store StoreBackend) {
	id, err := store.Set([]byte("foo"))
	if err != nil {
		t.Fatal(err)
	}
	id2, err := store.Set([]byte("bar"))
	if err != nil {
		t.Fatal(err)
	}

	tcases := make(map[digest.Digest]struct{})
	tcases[id] = struct{}{}
	tcases[id2] = struct{}{}
	n := 0
	err = store.Walk(func(id digest.Digest) error {
		delete(tcases, id)
		n++
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}

	if n != 2 {
		t.Fatalf("Expected 2 walk initializations, got %d", n)
	}
	if len(tcases) != 0 {
		t.Fatalf("Expected empty unwalked set, got %+v", tcases)
	}

	// stop on error
	tcases = make(map[digest.Digest]struct{})
	tcases[id] = struct{}{}
	err = store.Walk(func(id digest.Digest) error {
		return errors.New("")
	})
	if err == nil {
		t.Fatalf("Exected error from walker.")
	}
}
