package cli_test

import (
	"bytes"
	"encoding/json"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/kopia/kopia/internal/testutil"
	"github.com/kopia/kopia/snapshot"
	"github.com/kopia/kopia/snapshot/snapshotfs"
	"github.com/kopia/kopia/tests/testenv"
)

func TestSnapshotVerify(t *testing.T) {
	srcDir1 := testutil.TempDirectory(t)

	runner := testenv.NewInProcRunner(t)
	env := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner)

	env.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env.RepoDir)

	var intactMan, corruptMan1, corruptMan2 snapshot.Manifest

	// Write a file, create a new snapshot.
	intactFileName := "intact"
	mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, intactFileName), 1, bytes.Repeat([]byte{1, 2, 3}, 100))
	testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "snapshot", "create", srcDir1, "--json"), &intactMan)

	// Write a new file not present in the previous snapshot.
	corruptFileName1 := "corrupt1"
	pattern1 := []byte{1, 2, 4}
	mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, corruptFileName1), 1, bytes.Repeat(pattern1, 100))

	// Create a snapshot including the new file.
	testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "snapshot", "create", srcDir1, "--json"), &corruptMan1)

	// Write a new file not present in the previous two snapshots. Use a data pattern
	// distinct from the previous file to prevent dedup.
	corruptFileName2 := "corrupt2"
	pattern2 := []byte{1, 2, 5}
	mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, corruptFileName2), 1, bytes.Repeat(pattern2, 100))

	// Create a snapshot including the new file.
	testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "snapshot", "create", srcDir1, "--json"), &corruptMan2)

	// Corrupt the blobs containing the contents associated with the files to be corrupted.
	fileMap := mustGetFileMap(t, env, corruptMan2.RootObjectID())
	forgetContents(t, env, fileMap[corruptFileName1].ObjectID.String())
	forgetContents(t, env, fileMap[corruptFileName2].ObjectID.String())

	// Verifying everything is expected to fail.
	env.RunAndExpectFailure(t, "snapshot", "verify")

	// Verifying the untouched snapshot is expected to succeed.
	env.RunAndExpectSuccess(t, "snapshot", "verify", string(intactMan.ID))

	// Verifying the corrupted snapshot is expected to fail.
	env.RunAndExpectFailure(t, "snapshot", "verify", string(corruptMan1.ID))

	// Verifying the corrupted snapshot is expected to fail.
	env.RunAndExpectFailure(t, "snapshot", "verify", string(corruptMan2.ID))

	// Find one matching error corresponding to the single corrupted contents.
	_, stderr, err := env.Run(t, true, "snapshot", "verify", "--max-errors", "3", string(corruptMan1.ID))
	require.Error(t, err)
	assert.Equal(t, 1, strings.Count(strings.Join(stderr, "\n"), "error processing"))

	// Find two matching errors in the verify output, corresponding to each
	// of the two corrupted contents.
	_, stderr, err = env.Run(t, true, "snapshot", "verify", "--max-errors", "3", string(corruptMan2.ID))
	require.Error(t, err)
	assert.Equal(t, 2, strings.Count(strings.Join(stderr, "\n"), "error processing"))

	// Requesting a snapshot verify of a non-existent manifest ID results in error.
	env.RunAndExpectFailure(t, "snapshot", "verify", "not-a-manifest-id")

	// Redo the above snapshot verify commands with the --json flag and check the outputs.

	// Verifying everything is expected to fail.
	stdout, _ := env.RunAndExpectFailure(t, "snapshot", "verify", "--json")
	require.Len(t, stdout, 1)
	result := unmarshalSnapVerify(t, stdout[0])
	require.NotZero(t, result.Stats.ProcessedObjectCount)
	require.NotZero(t, result.ErrorCount)
	require.Len(t, result.ErrorStrings, 1)

	// Verifying the untouched snapshot is expected to succeed.
	stdout = env.RunAndExpectSuccess(t, "snapshot", "verify", "--json", string(intactMan.ID))
	require.Len(t, stdout, 1)
	result = unmarshalSnapVerify(t, stdout[0])
	require.NotZero(t, result.Stats.ProcessedObjectCount)
	require.Zero(t, result.ErrorCount)
	require.Empty(t, result.ErrorStrings)

	// Verifying the corrupted snapshot is expected to fail.
	stdout, _ = env.RunAndExpectFailure(t, "snapshot", "verify", "--json", string(corruptMan1.ID))
	require.Len(t, stdout, 1)
	result = unmarshalSnapVerify(t, stdout[0])
	require.NotZero(t, result.Stats.ProcessedObjectCount)
	require.NotZero(t, result.ErrorCount)
	require.Len(t, result.ErrorStrings, 1)

	// Verifying the corrupted snapshot is expected to fail.
	stdout, _ = env.RunAndExpectFailure(t, "snapshot", "verify", "--json", string(corruptMan2.ID))
	require.Len(t, stdout, 1)
	result = unmarshalSnapVerify(t, stdout[0])
	require.NotZero(t, result.Stats.ProcessedObjectCount)
	require.NotZero(t, result.ErrorCount)
	require.Len(t, result.ErrorStrings, 1)

	// Find one matching error corresponding to the single corrupted contents.
	stdout, stderr, err = env.Run(t, true, "snapshot", "verify", "--json", "--max-errors", "3", string(corruptMan1.ID))
	require.Error(t, err)
	assert.Equal(t, 1, strings.Count(strings.Join(stderr, "\n"), "error processing"))
	result = unmarshalSnapVerify(t, stdout[0])
	require.NotZero(t, result.Stats.ProcessedObjectCount)
	require.Equal(t, 1, result.ErrorCount)
	require.Len(t, result.ErrorStrings, 1)

	// Find two matching errors in the verify output, corresponding to each
	// of the two corrupted contents.
	stdout, stderr, err = env.Run(t, true, "snapshot", "verify", "--json", "--max-errors", "3", string(corruptMan2.ID))
	require.Error(t, err)
	assert.Equal(t, 2, strings.Count(strings.Join(stderr, "\n"), "error processing"))
	result = unmarshalSnapVerify(t, stdout[0])
	require.NotZero(t, result.Stats.ProcessedObjectCount)
	require.Equal(t, 2, result.ErrorCount)
	require.Len(t, result.ErrorStrings, 2)

	// Requesting a snapshot verify of a non-existent manifest ID results in error.
	stdout, _ = env.RunAndExpectFailure(t, "snapshot", "verify", "--json", "not-a-manifest-id")
	result = unmarshalSnapVerify(t, stdout[0])
	require.Zero(t, result.Stats.ProcessedObjectCount)
	require.Equal(t, 1, result.ErrorCount)
	require.Len(t, result.ErrorStrings, 1)
}

func unmarshalSnapVerify(t *testing.T, stdout string) *snapshotfs.VerifierResult {
	t.Helper()

	r := &snapshotfs.VerifierResult{}
	require.NoError(t, json.Unmarshal([]byte(stdout), r))

	return r
}
