package plumbing

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/src-d/go-git.v4/plumbing"
	"gopkg.in/src-d/go-git.v4/plumbing/object"
	"gopkg.in/src-d/hercules.v6/internal"
	"gopkg.in/src-d/hercules.v6/internal/core"
	"gopkg.in/src-d/hercules.v6/internal/test"
)

func fixtureBlobCache() *BlobCache {
	cache := &BlobCache{}
	cache.Initialize(test.Repository)
	return cache
}

func AddHash(t *testing.T, cache map[plumbing.Hash]*CachedBlob, hash string) {
	objhash := plumbing.NewHash(hash)
	blob, err := test.Repository.BlobObject(objhash)
	assert.Nil(t, err)
	cb := &CachedBlob{Blob: *blob}
	err = cb.Cache()
	assert.Nil(t, err)
	cache[objhash] = cb
}

func TestBlobCacheConfigureInitialize(t *testing.T) {
	cache := fixtureBlobCache()
	assert.Equal(t, test.Repository, cache.repository)
	assert.False(t, cache.FailOnMissingSubmodules)
	facts := map[string]interface{}{}
	facts[ConfigBlobCacheFailOnMissingSubmodules] = true
	cache.Configure(facts)
	assert.True(t, cache.FailOnMissingSubmodules)
	facts = map[string]interface{}{}
	cache.Configure(facts)
	assert.True(t, cache.FailOnMissingSubmodules)
}

func TestBlobCacheMetadata(t *testing.T) {
	cache := fixtureBlobCache()
	assert.Equal(t, cache.Name(), "BlobCache")
	assert.Equal(t, len(cache.Provides()), 1)
	assert.Equal(t, cache.Provides()[0], DependencyBlobCache)
	assert.Equal(t, len(cache.Requires()), 1)
	changes := &TreeDiff{}
	assert.Equal(t, cache.Requires()[0], changes.Provides()[0])
	opts := cache.ListConfigurationOptions()
	assert.Len(t, opts, 1)
	assert.Equal(t, opts[0].Name, ConfigBlobCacheFailOnMissingSubmodules)
}

func TestBlobCacheRegistration(t *testing.T) {
	summoned := core.Registry.Summon((&BlobCache{}).Name())
	assert.Len(t, summoned, 1)
	assert.Equal(t, summoned[0].Name(), "BlobCache")
	summoned = core.Registry.Summon((&BlobCache{}).Provides()[0])
	assert.Len(t, summoned, 1)
	assert.Equal(t, summoned[0].Name(), "BlobCache")
}

func TestBlobCacheConsumeModification(t *testing.T) {
	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
		"af2d8db70f287b52d2428d9887a69a10bc4d1f46"))
	changes := make(object.Changes, 1)
	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
		"80fe25955b8e725feee25c08ea5759d74f8b670d"))
	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
		"63076fa0dfd93e94b6d2ef0fc8b1fdf9092f83c4"))
	changes[0] = &object.Change{From: object.ChangeEntry{
		Name: "labours.py",
		Tree: treeFrom,
		TreeEntry: object.TreeEntry{
			Name: "labours.py",
			Mode: 0100644,
			Hash: plumbing.NewHash("1cacfc1bf0f048eb2f31973750983ae5d8de647a"),
		},
	}, To: object.ChangeEntry{
		Name: "labours.py",
		Tree: treeTo,
		TreeEntry: object.TreeEntry{
			Name: "labours.py",
			Mode: 0100644,
			Hash: plumbing.NewHash("c872b8d2291a5224e2c9f6edd7f46039b96b4742"),
		},
	}}
	deps := map[string]interface{}{}
	deps[core.DependencyCommit] = commit
	deps[DependencyTreeChanges] = changes
	result, err := fixtureBlobCache().Consume(deps)
	assert.Nil(t, err)
	assert.Equal(t, len(result), 1)
	cacheIface, exists := result[DependencyBlobCache]
	assert.True(t, exists)
	cache := cacheIface.(map[plumbing.Hash]*CachedBlob)
	assert.Equal(t, len(cache), 2)
	blobFrom, exists := cache[plumbing.NewHash("1cacfc1bf0f048eb2f31973750983ae5d8de647a")]
	assert.True(t, exists)
	blobTo, exists := cache[plumbing.NewHash("c872b8d2291a5224e2c9f6edd7f46039b96b4742")]
	assert.True(t, exists)
	assert.Equal(t, blobFrom.Size, int64(8969))
	assert.Equal(t, blobTo.Size, int64(9481))
}

func TestBlobCacheConsumeInsertionDeletion(t *testing.T) {
	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
	changes := make(object.Changes, 2)
	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
		"96c6ece9b2f3c7c51b83516400d278dea5605100"))
	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
		"251f2094d7b523d5bcc60e663b6cf38151bf8844"))
	changes[0] = &object.Change{From: object.ChangeEntry{
		Name: "analyser.go",
		Tree: treeFrom,
		TreeEntry: object.TreeEntry{
			Name: "analyser.go",
			Mode: 0100644,
			Hash: plumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1"),
		},
	}, To: object.ChangeEntry{},
	}
	changes[1] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
		Name: "pipeline.go",
		Tree: treeTo,
		TreeEntry: object.TreeEntry{
			Name: "pipeline.go",
			Mode: 0100644,
			Hash: plumbing.NewHash("db99e1890f581ad69e1527fe8302978c661eb473"),
		},
	},
	}
	deps := map[string]interface{}{}
	deps[core.DependencyCommit] = commit
	deps[DependencyTreeChanges] = changes
	result, err := fixtureBlobCache().Consume(deps)
	assert.Nil(t, err)
	assert.Equal(t, len(result), 1)
	cacheIface, exists := result[DependencyBlobCache]
	assert.True(t, exists)
	cache := cacheIface.(map[plumbing.Hash]*CachedBlob)
	assert.Equal(t, len(cache), 2)
	blobFrom, exists := cache[plumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1")]
	assert.True(t, exists)
	blobTo, exists := cache[plumbing.NewHash("db99e1890f581ad69e1527fe8302978c661eb473")]
	assert.True(t, exists)
	assert.Equal(t, blobFrom.Size, int64(26446))
	assert.Equal(t, blobTo.Size, int64(5576))
}

func TestBlobCacheConsumeNoAction(t *testing.T) {
	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
		"af2d8db70f287b52d2428d9887a69a10bc4d1f46"))
	changes := make(object.Changes, 1)
	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
		"80fe25955b8e725feee25c08ea5759d74f8b670d"))
	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
		"63076fa0dfd93e94b6d2ef0fc8b1fdf9092f83c4"))
	changes[0] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{}}
	deps := map[string]interface{}{}
	deps[core.DependencyCommit] = commit
	deps[DependencyTreeChanges] = changes
	result, err := fixtureBlobCache().Consume(deps)
	assert.Nil(t, result)
	assert.NotNil(t, err)
	changes[0] = &object.Change{From: object.ChangeEntry{
		Name:      "labours.py",
		Tree:      treeFrom,
		TreeEntry: object.TreeEntry{},
	}, To: object.ChangeEntry{
		Name:      "labours.py",
		Tree:      treeTo,
		TreeEntry: object.TreeEntry{},
	}}
	result, err = fixtureBlobCache().Consume(deps)
	assert.Nil(t, result)
	assert.NotNil(t, err)
}

func TestBlobCacheConsumeBadHashes(t *testing.T) {
	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
		"af2d8db70f287b52d2428d9887a69a10bc4d1f46"))
	changes := make(object.Changes, 1)
	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
		"80fe25955b8e725feee25c08ea5759d74f8b670d"))
	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
		"63076fa0dfd93e94b6d2ef0fc8b1fdf9092f83c4"))
	changes[0] = &object.Change{From: object.ChangeEntry{
		Name:      "labours.py",
		Tree:      treeFrom,
		TreeEntry: object.TreeEntry{},
	}, To: object.ChangeEntry{
		Name:      "labours.py",
		Tree:      treeTo,
		TreeEntry: object.TreeEntry{},
	}}
	deps := map[string]interface{}{}
	deps[core.DependencyCommit] = commit
	deps[DependencyTreeChanges] = changes
	result, err := fixtureBlobCache().Consume(deps)
	assert.Nil(t, result)
	assert.NotNil(t, err)
	changes[0] = &object.Change{From: object.ChangeEntry{
		Name:      "labours.py",
		Tree:      treeFrom,
		TreeEntry: object.TreeEntry{},
	}, To: object.ChangeEntry{}}
	result, err = fixtureBlobCache().Consume(deps)
	// Deleting a missing blob is fine
	assert.NotNil(t, result)
	assert.Nil(t, err)
	changes[0] = &object.Change{From: object.ChangeEntry{},
		To: object.ChangeEntry{
			Name:      "labours.py",
			Tree:      treeTo,
			TreeEntry: object.TreeEntry{},
		}}
	result, err = fixtureBlobCache().Consume(deps)
	assert.Nil(t, result)
	assert.NotNil(t, err)
}

func TestBlobCacheConsumeInvalidHash(t *testing.T) {
	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
		"af2d8db70f287b52d2428d9887a69a10bc4d1f46"))
	changes := make(object.Changes, 1)
	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
		"80fe25955b8e725feee25c08ea5759d74f8b670d"))
	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
		"63076fa0dfd93e94b6d2ef0fc8b1fdf9092f83c4"))
	changes[0] = &object.Change{From: object.ChangeEntry{
		Name: "labours.py",
		Tree: treeFrom,
		TreeEntry: object.TreeEntry{
			Name: "labours.py",
			Mode: 0100644,
			Hash: plumbing.NewHash("ffffffffffffffffffffffffffffffffffffffff"),
		},
	}, To: object.ChangeEntry{
		Name:      "labours.py",
		Tree:      treeTo,
		TreeEntry: object.TreeEntry{},
	}}
	deps := map[string]interface{}{}
	deps[core.DependencyCommit] = commit
	deps[DependencyTreeChanges] = changes
	result, err := fixtureBlobCache().Consume(deps)
	assert.Nil(t, result)
	assert.NotNil(t, err)
}

func TestBlobCacheGetBlob(t *testing.T) {
	cache := fixtureBlobCache()
	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
		"80fe25955b8e725feee25c08ea5759d74f8b670d"))
	entry := object.ChangeEntry{
		Name: "labours.py",
		Tree: treeFrom,
		TreeEntry: object.TreeEntry{
			Name: "labours.py",
			Mode: 0100644,
			Hash: plumbing.NewHash("80fe25955b8e725feee25c08ea5759d74f8b670d"),
		},
	}
	getter := func(path string) (*object.File, error) {
		assert.Equal(t, path, ".gitmodules")
		commit, _ := test.Repository.CommitObject(plumbing.NewHash(
			"13272b66c55e1ba1237a34104f30b84d7f6e4082"))
		return commit.File("test_data/gitmodules")
	}
	blob, err := cache.getBlob(&entry, getter)
	assert.Nil(t, blob)
	assert.NotNil(t, err)
	assert.Equal(t, err.Error(), plumbing.ErrObjectNotFound.Error())
	getter = func(path string) (*object.File, error) {
		assert.Equal(t, path, ".gitmodules")
		commit, _ := test.Repository.CommitObject(plumbing.NewHash(
			"13272b66c55e1ba1237a34104f30b84d7f6e4082"))
		return commit.File("test_data/gitmodules_empty")
	}
	blob, err = cache.getBlob(&entry, getter)
	assert.Nil(t, blob)
	assert.NotNil(t, err)
	assert.Equal(t, err.Error(), plumbing.ErrObjectNotFound.Error())
}

func TestBlobCacheDeleteInvalidBlob(t *testing.T) {
	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
	changes := make(object.Changes, 1)
	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
		"96c6ece9b2f3c7c51b83516400d278dea5605100"))
	changes[0] = &object.Change{From: object.ChangeEntry{
		Name: "analyser.go",
		Tree: treeFrom,
		TreeEntry: object.TreeEntry{
			Name: "analyser.go",
			Mode: 0100644,
			Hash: plumbing.NewHash("ffffffffffffffffffffffffffffffffffffffff"),
		},
	}, To: object.ChangeEntry{},
	}
	deps := map[string]interface{}{}
	deps[core.DependencyCommit] = commit
	deps[DependencyTreeChanges] = changes
	result, err := fixtureBlobCache().Consume(deps)
	assert.Nil(t, err)
	assert.Equal(t, len(result), 1)
	cacheIface, exists := result[DependencyBlobCache]
	assert.True(t, exists)
	cache := cacheIface.(map[plumbing.Hash]*CachedBlob)
	assert.Equal(t, len(cache), 1)
	blobFrom, exists := cache[plumbing.NewHash("ffffffffffffffffffffffffffffffffffffffff")]
	assert.True(t, exists)
	assert.Equal(t, blobFrom.Size, int64(0))
}

func TestBlobCacheInsertInvalidBlob(t *testing.T) {
	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
	changes := make(object.Changes, 1)
	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
		"251f2094d7b523d5bcc60e663b6cf38151bf8844"))
	changes[0] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
		Name: "pipeline.go",
		Tree: treeTo,
		TreeEntry: object.TreeEntry{
			Name: "pipeline.go",
			Mode: 0100644,
			Hash: plumbing.NewHash("ffffffffffffffffffffffffffffffffffffffff"),
		},
	},
	}
	deps := map[string]interface{}{}
	deps[core.DependencyCommit] = commit
	deps[DependencyTreeChanges] = changes
	result, err := fixtureBlobCache().Consume(deps)
	assert.NotNil(t, err)
	assert.Equal(t, len(result), 0)
}

func TestBlobCacheGetBlobIgnoreMissing(t *testing.T) {
	cache := fixtureBlobCache()
	cache.FailOnMissingSubmodules = false
	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
		"80fe25955b8e725feee25c08ea5759d74f8b670d"))
	entry := object.ChangeEntry{
		Name: core.DependencyCommit,
		Tree: treeFrom,
		TreeEntry: object.TreeEntry{
			Name: core.DependencyCommit,
			Mode: 0160000,
			Hash: plumbing.NewHash("ffffffffffffffffffffffffffffffffffffffff"),
		},
	}
	getter := func(path string) (*object.File, error) {
		return nil, plumbing.ErrObjectNotFound
	}
	blob, err := cache.getBlob(&entry, getter)
	assert.NotNil(t, blob)
	assert.Nil(t, err)
	assert.Equal(t, blob.Size, int64(0))
	cache.FailOnMissingSubmodules = true
	getter = func(path string) (*object.File, error) {
		assert.Equal(t, path, ".gitmodules")
		commit, _ := test.Repository.CommitObject(plumbing.NewHash(
			"13272b66c55e1ba1237a34104f30b84d7f6e4082"))
		return commit.File("test_data/gitmodules")
	}
	blob, err = cache.getBlob(&entry, getter)
	assert.Nil(t, blob)
	assert.NotNil(t, err)
}

func TestBlobCacheGetBlobGitModulesErrors(t *testing.T) {
	cache := fixtureBlobCache()
	cache.FailOnMissingSubmodules = true
	entry := object.ChangeEntry{
		Name: "labours.py",
		TreeEntry: object.TreeEntry{
			Name: "labours.py",
			Mode: 0160000,
			Hash: plumbing.NewHash("ffffffffffffffffffffffffffffffffffffffff"),
		},
	}
	getter := func(path string) (*object.File, error) {
		return nil, plumbing.ErrInvalidType
	}
	blob, err := cache.getBlob(&entry, getter)
	assert.Nil(t, blob)
	assert.NotNil(t, err)
	assert.Equal(t, err.Error(), plumbing.ErrInvalidType.Error())
	getter = func(path string) (*object.File, error) {
		blob, _ := internal.CreateDummyBlob(
			plumbing.NewHash("ffffffffffffffffffffffffffffffffffffffff"), true)
		return &object.File{Name: "fake", Blob: *blob}, nil
	}
	blob, err = cache.getBlob(&entry, getter)
	assert.Nil(t, blob)
	assert.NotNil(t, err)
	assert.Equal(t, err.Error(), "dummy failure")
	getter = func(path string) (*object.File, error) {
		blob, _ := test.Repository.BlobObject(plumbing.NewHash(
			"4434197c2b0509d990f09d53a3cabb910bfd34b7"))
		return &object.File{Name: ".gitmodules", Blob: *blob}, nil
	}
	blob, err = cache.getBlob(&entry, getter)
	assert.Nil(t, blob)
	assert.NotNil(t, err)
	assert.NotEqual(t, err.Error(), plumbing.ErrObjectNotFound.Error())
}

func TestBlobCacheFork(t *testing.T) {
	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
	changes := make(object.Changes, 1)
	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
		"251f2094d7b523d5bcc60e663b6cf38151bf8844"))
	hash := plumbing.NewHash("db99e1890f581ad69e1527fe8302978c661eb473")
	changes[0] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
		Name: "pipeline.go",
		Tree: treeTo,
		TreeEntry: object.TreeEntry{
			Name: "pipeline.go",
			Mode: 0100644,
			Hash: hash,
		},
	}}
	deps := map[string]interface{}{}
	deps[core.DependencyCommit] = commit
	deps[DependencyTreeChanges] = changes
	cache1 := fixtureBlobCache()
	cache1.FailOnMissingSubmodules = true
	cache1.Consume(deps)
	clones := cache1.Fork(1)
	assert.Len(t, clones, 1)
	cache2 := clones[0].(*BlobCache)
	assert.True(t, cache2.FailOnMissingSubmodules)
	assert.Equal(t, cache1.repository, cache2.repository)
	cache1.cache[plumbing.ZeroHash] = nil
	assert.Len(t, cache1.cache, 2)
	assert.Len(t, cache2.cache, 1)
	assert.Equal(t, cache1.cache[hash].Size, cache2.cache[hash].Size)
	// just for the sake of it
	cache1.Merge([]core.PipelineItem{cache2})
}
