package chanbackup

import (
	"bytes"
	"fmt"
	"math/rand"
	"os"
	"path/filepath"
	"testing"

	"github.com/lightningnetwork/lnd/lnencrypt"
	"github.com/stretchr/testify/require"
)

func makeFakePackedMulti() (PackedMulti, error) {
	newPackedMulti := make([]byte, 50)
	if _, err := rand.Read(newPackedMulti[:]); err != nil {
		return nil, fmt.Errorf("unable to make test backup: %w", err)
	}

	return PackedMulti(newPackedMulti), nil
}

func assertBackupMatches(t *testing.T, filePath string,
	currentBackup PackedMulti) {

	t.Helper()

	packedBackup, err := os.ReadFile(filePath)
	require.NoError(t, err, "unable to test file")

	if !bytes.Equal(packedBackup, currentBackup) {
		t.Fatalf("backups don't match after first swap: "+
			"expected %x got %x", packedBackup[:],
			currentBackup)
	}
}

func assertFileDeleted(t *testing.T, filePath string) {
	t.Helper()

	_, err := os.Stat(filePath)
	if err == nil {
		t.Fatalf("file %v still exists: ", filePath)
	}
}

// TestUpdateAndSwap test that we're able to properly swap out old backups on
// disk with new ones. Additionally, after a swap operation succeeds, then each
// time we should only have the main backup file on disk, as the temporary file
// has been removed.
func TestUpdateAndSwap(t *testing.T) {
	t.Parallel()

	tempTestDir := t.TempDir()

	testCases := []struct {
		fileName     string
		tempFileName string

		oldTempExists bool

		valid bool
	}{
		// Main file name is blank, should fail.
		{
			fileName: "",
			valid:    false,
		},

		// Old temporary file still exists, should be removed. Only one
		// file should remain.
		{
			fileName: filepath.Join(
				tempTestDir, DefaultBackupFileName,
			),
			tempFileName: filepath.Join(
				tempTestDir, DefaultTempBackupFileName,
			),
			oldTempExists: true,
			valid:         true,
		},

		// Old temp doesn't exist, should swap out file, only a single
		// file remains.
		{
			fileName: filepath.Join(
				tempTestDir, DefaultBackupFileName,
			),
			tempFileName: filepath.Join(
				tempTestDir, DefaultTempBackupFileName,
			),
			valid: true,
		},
	}
	for i, testCase := range testCases {
		backupFile := NewMultiFile(testCase.fileName)

		// To start with, we'll make a random byte slice that'll pose
		// as our packed multi backup.
		newPackedMulti, err := makeFakePackedMulti()
		if err != nil {
			t.Fatalf("unable to make test backup: %v", err)
		}

		// If the old temporary file is meant to exist, then we'll
		// create it now as an empty file.
		if testCase.oldTempExists {
			f, err := os.Create(testCase.tempFileName)
			if err != nil {
				t.Fatalf("unable to create temp file: %v", err)
			}
			require.NoError(t, f.Close())

			// TODO(roasbeef): mock out fs calls?
		}

		// With our backup created, we'll now attempt to swap out this
		// backup, for the old one.
		err = backupFile.UpdateAndSwap(PackedMulti(newPackedMulti))
		switch {
		// If this is a valid test case, and we failed, then we'll
		// return an error.
		case err != nil && testCase.valid:
			t.Fatalf("#%v, unable to swap file: %v", i, err)

		// If this is an invalid test case, and we passed it, then
		// we'll return an error.
		case err == nil && !testCase.valid:
			t.Fatalf("#%v file swap should have failed: %v", i, err)
		}

		if !testCase.valid {
			continue
		}

		// If we read out the file on disk, then it should match
		// exactly what we wrote. The temp backup file should also be
		// gone.
		assertBackupMatches(t, testCase.fileName, newPackedMulti)
		assertFileDeleted(t, testCase.tempFileName)

		// Now that we know this is a valid test case, we'll make a new
		// packed multi to swap out this current one.
		newPackedMulti2, err := makeFakePackedMulti()
		if err != nil {
			t.Fatalf("unable to make test backup: %v", err)
		}

		// We'll then attempt to swap the old version for this new one.
		err = backupFile.UpdateAndSwap(PackedMulti(newPackedMulti2))
		if err != nil {
			t.Fatalf("unable to swap file: %v", err)
		}

		// Once again, the file written on disk should have been
		// properly swapped out with the new instance.
		assertBackupMatches(t, testCase.fileName, newPackedMulti2)

		// Additionally, we shouldn't be able to find the temp backup
		// file on disk, as it should be deleted each time.
		assertFileDeleted(t, testCase.tempFileName)
	}
}

func assertMultiEqual(t *testing.T, a, b *Multi) {

	if len(a.StaticBackups) != len(b.StaticBackups) {
		t.Fatalf("expected %v backups, got %v", len(a.StaticBackups),
			len(b.StaticBackups))
	}

	for i := 0; i < len(a.StaticBackups); i++ {
		assertSingleEqual(t, a.StaticBackups[i], b.StaticBackups[i])
	}
}

// TestExtractMulti tests that given a valid packed multi file on disk, we're
// able to read it multiple times repeatedly.
func TestExtractMulti(t *testing.T) {
	t.Parallel()

	keyRing := &lnencrypt.MockKeyRing{}

	// First, as prep, we'll create a single chan backup, then pack that
	// fully into a multi backup.
	channel, err := genRandomOpenChannelShell()
	require.NoError(t, err, "unable to gen chan")

	singleBackup := NewSingle(channel, nil)

	var b bytes.Buffer
	unpackedMulti := Multi{
		StaticBackups: []Single{singleBackup},
	}
	err = unpackedMulti.PackToWriter(&b, keyRing)
	require.NoError(t, err, "unable to pack to writer")

	packedMulti := PackedMulti(b.Bytes())

	// Finally, we'll make a new temporary file, then write out the packed
	// multi directly to it.
	tempFile, err := os.CreateTemp("", "")
	require.NoError(t, err, "unable to create temp file")
	t.Cleanup(func() {
		require.NoError(t, tempFile.Close())
		require.NoError(t, os.Remove(tempFile.Name()))
	})

	_, err = tempFile.Write(packedMulti)
	require.NoError(t, err, "unable to write temp file")
	if err := tempFile.Sync(); err != nil {
		t.Fatalf("unable to sync temp file: %v", err)
	}

	testCases := []struct {
		fileName string
		pass     bool
	}{
		// Main file not read, file name not present.
		{
			fileName: "",
			pass:     false,
		},

		// Main file not read, file name is there, but file doesn't
		// exist.
		{
			fileName: "kek",
			pass:     false,
		},

		// Main file not read, should be able to read multiple times.
		{
			fileName: tempFile.Name(),
			pass:     true,
		},
	}
	for i, testCase := range testCases {
		// First, we'll make our backup file with the specified name.
		backupFile := NewMultiFile(testCase.fileName)

		// With our file made, we'll now attempt to read out the
		// multi-file.
		freshUnpackedMulti, err := backupFile.ExtractMulti(keyRing)
		switch {
		// If this is a valid test case, and we failed, then we'll
		// return an error.
		case err != nil && testCase.pass:
			t.Fatalf("#%v, unable to extract file: %v", i, err)

			// If this is an invalid test case, and we passed it, then
			// we'll return an error.
		case err == nil && !testCase.pass:
			t.Fatalf("#%v file extraction should have "+
				"failed: %v", i, err)
		}

		if !testCase.pass {
			continue
		}

		// We'll now ensure that the unpacked multi we read is
		// identical to the one we wrote out above.
		assertMultiEqual(t, &unpackedMulti, freshUnpackedMulti)

		// We should also be able to read the file again, as we have an
		// existing handle to it.
		freshUnpackedMulti, err = backupFile.ExtractMulti(keyRing)
		if err != nil {
			t.Fatalf("unable to unpack multi: %v", err)
		}

		assertMultiEqual(t, &unpackedMulti, freshUnpackedMulti)
	}
}
