package storage

import (
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/theupdateframework/notary/tuf/data"
	"github.com/theupdateframework/notary/tuf/testutils"
)

// Produce a series of tufMeta objects and updates given a TUF repo
func metaFromRepo(t *testing.T, gun data.GUN, version int) map[string]StoredTUFMeta {
	tufRepo, _, err := testutils.EmptyRepo(gun, "targets/a", "targets/a/b")
	require.NoError(t, err)

	tufRepo.Root.Signed.Version = version - 1
	tufRepo.Timestamp.Signed.Version = version - 1
	tufRepo.Snapshot.Signed.Version = version - 1
	for _, signedObj := range tufRepo.Targets {
		signedObj.Signed.Version = version - 1
	}

	metaBytes, err := testutils.SignAndSerialize(tufRepo)
	require.NoError(t, err)

	tufMeta := make(map[string]StoredTUFMeta)
	for role, tufdata := range metaBytes {
		tufMeta[role.String()] = SampleCustomTUFObj(gun, role, version, tufdata)
	}

	return tufMeta
}

// TUFMetaStore's GetCurrent walks from the current timestamp metadata
// to the snapshot specified in the checksum, to potentially other role metadata by checksum
func testTUFMetaStoreGetCurrent(t *testing.T, s MetaStore) {
	tufDBStore := NewTUFMetaStorage(s)
	var gun data.GUN = "testGUN"

	initialRootTUF := SampleCustomTUFObj(gun, data.CanonicalRootRole, 1, nil)
	ConsistentEmptyGetCurrentTest(t, tufDBStore, initialRootTUF)

	// put an initial piece of root metadata data in the database,
	// there isn't enough state to retrieve it since we require a timestamp and snapshot in our walk
	require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(initialRootTUF)), "Adding root to empty store failed.")
	ConsistentMissingTSAndSnapGetCurrentTest(t, tufDBStore, initialRootTUF)

	// Note that get by checksum succeeds, since it does not try to walk timestamp/snapshot
	_, _, err := tufDBStore.GetChecksum(gun, data.CanonicalRootRole, initialRootTUF.SHA256)
	require.NoError(t, err)

	// Now add metadata from a valid TUF repo to ensure that we walk correctly.
	tufMetaByRole := metaFromRepo(t, gun, 2)
	updates := make([]MetaUpdate, 0, len(tufMetaByRole))
	for _, tufObj := range tufMetaByRole {
		updates = append(updates, MakeUpdate(tufObj))
	}
	require.NoError(t, s.UpdateMany(gun, updates))

	// GetCurrent on all of these roles should succeed
	for _, tufobj := range tufMetaByRole {
		ConsistentGetCurrentFoundTest(t, tufDBStore, tufobj)
	}

	// Delete snapshot by just wiping out everything in the store and adding only
	// the non-snapshot data
	require.NoError(t, s.Delete(gun), "unable to delete metadata")
	updates = make([]MetaUpdate, 0, len(updates)-1)
	for role, tufObj := range tufMetaByRole {
		if role != data.CanonicalSnapshotRole.String() {
			updates = append(updates, MakeUpdate(tufObj))
		}
	}
	require.NoError(t, s.UpdateMany(gun, updates))
	_, _, err = s.GetCurrent(gun, data.CanonicalSnapshotRole)
	require.IsType(t, ErrNotFound{}, err)

	// GetCurrent on all roles should still succeed - snapshot lookup because of caching,
	// and targets and root because the snapshot is cached
	for _, tufobj := range tufMetaByRole {
		ConsistentGetCurrentFoundTest(t, tufDBStore, tufobj)
	}

	// add another orphaned root, but ensure that we still get the previous root
	// since the new root isn't in a timestamp/snapshot chain
	orphanedRootTUF := SampleCustomTUFObj(gun, data.CanonicalRootRole, 3, []byte("orphanedRoot"))
	require.NoError(t, s.UpdateCurrent(gun, MakeUpdate(orphanedRootTUF)), "unable to create orphaned root in store")

	// a GetCurrent for this gun and root gets us the previous root, which is linked in timestamp and snapshot
	ConsistentGetCurrentFoundTest(t, tufDBStore, tufMetaByRole[data.CanonicalRootRole.String()])
	// the orphaned root fails on a GetCurrent even though it's in the underlying store
	ConsistentTSAndSnapGetDifferentCurrentTest(t, tufDBStore, orphanedRootTUF)
}

func ConsistentGetCurrentFoundTest(t *testing.T, s *TUFMetaStorage, rec StoredTUFMeta) {
	_, byt, err := s.GetCurrent(rec.Gun, rec.Role)
	require.NoError(t, err)
	require.Equal(t, rec.Data, byt)
}

// Checks that both the walking metastore and underlying metastore do not contain the TUF file
func ConsistentEmptyGetCurrentTest(t *testing.T, s *TUFMetaStorage, rec StoredTUFMeta) {
	_, byt, err := s.GetCurrent(rec.Gun, rec.Role)
	require.Nil(t, byt)
	require.Error(t, err, "There should be an error getting an empty table")
	require.IsType(t, ErrNotFound{}, err, "Should get a not found error")

	_, byt, err = s.MetaStore.GetCurrent(rec.Gun, rec.Role)
	require.Nil(t, byt)
	require.Error(t, err, "There should be an error getting an empty table")
	require.IsType(t, ErrNotFound{}, err, "Should get a not found error")
}

// Check that we can't get the "current" specified role because we can't walk from timestamp --> snapshot --> role
// Also checks that the role metadata still exists in the underlying store
func ConsistentMissingTSAndSnapGetCurrentTest(t *testing.T, s *TUFMetaStorage, rec StoredTUFMeta) {
	_, byt, err := s.GetCurrent(rec.Gun, rec.Role)
	require.Nil(t, byt)
	require.Error(t, err, "There should be an error because there is no timestamp or snapshot to use on GetCurrent")
	_, byt, err = s.MetaStore.GetCurrent(rec.Gun, rec.Role)
	require.Equal(t, rec.Data, byt)
	require.NoError(t, err)
}

// Check that we can get the "current" specified role but it is different from the provided TUF file because
// the most valid walk from timestamp --> snapshot --> role gets a different
func ConsistentTSAndSnapGetDifferentCurrentTest(t *testing.T, s *TUFMetaStorage, rec StoredTUFMeta) {
	_, byt, err := s.GetCurrent(rec.Gun, rec.Role)
	require.NotEqual(t, rec.Data, byt)
	require.NoError(t, err)
	_, byt, err = s.MetaStore.GetCurrent(rec.Gun, rec.Role)
	require.Equal(t, rec.Data, byt)
	require.NoError(t, err)
}
