package main

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/restic/restic/internal/global"
	"github.com/restic/restic/internal/restic"
	rtest "github.com/restic/restic/internal/test"
	"github.com/restic/restic/internal/ui"
)

func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts global.Options, pattern string) []byte {
	buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
		gopts.JSON = wantJSON

		return runFind(ctx, opts, gopts, []string{pattern}, gopts.Term)
	})
	rtest.OK(t, err)
	return buf.Bytes()
}

func TestFind(t *testing.T) {
	env, cleanup := withTestEnvironment(t)
	defer cleanup()

	datafile := testSetupBackupData(t, env)
	opts := BackupOptions{}

	testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)

	results := testRunFind(t, false, FindOptions{}, env.gopts, "unexistingfile")
	rtest.Assert(t, len(results) == 0, "unexisting file found in repo (%v)", datafile)

	results = testRunFind(t, false, FindOptions{}, env.gopts, "testfile")
	lines := strings.Split(string(results), "\n")
	rtest.Assert(t, len(lines) == 2, "expected one file found in repo (%v)", datafile)

	results = testRunFind(t, false, FindOptions{}, env.gopts, "testfile*")
	lines = strings.Split(string(results), "\n")
	rtest.Assert(t, len(lines) == 4, "expected three files found in repo (%v)", datafile)
}

type testMatch struct {
	Path        string    `json:"path,omitempty"`
	Permissions string    `json:"permissions,omitempty"`
	Size        uint64    `json:"size,omitempty"`
	Date        time.Time `json:"date,omitempty"`
	UID         uint32    `json:"uid,omitempty"`
	GID         uint32    `json:"gid,omitempty"`
}

type testMatches struct {
	Hits       int         `json:"hits,omitempty"`
	SnapshotID string      `json:"snapshot,omitempty"`
	Matches    []testMatch `json:"matches,omitempty"`
}

func TestFindJSON(t *testing.T) {
	env, cleanup := withTestEnvironment(t)
	defer cleanup()

	datafile := testSetupBackupData(t, env)
	opts := BackupOptions{}

	testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
	testRunCheck(t, env.gopts)
	snapshot, _ := testRunSnapshots(t, env.gopts)

	results := testRunFind(t, true, FindOptions{}, env.gopts, "unexistingfile")
	matches := []testMatches{}
	rtest.OK(t, json.Unmarshal(results, &matches))
	rtest.Assert(t, len(matches) == 0, "expected no match in repo (%v)", datafile)

	results = testRunFind(t, true, FindOptions{}, env.gopts, "testfile")
	rtest.OK(t, json.Unmarshal(results, &matches))
	rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
	rtest.Assert(t, len(matches[0].Matches) == 1, "expected a single file to match (%v)", datafile)
	rtest.Assert(t, matches[0].Hits == 1, "expected hits to show 1 match (%v)", datafile)

	results = testRunFind(t, true, FindOptions{}, env.gopts, "testfile*")
	rtest.OK(t, json.Unmarshal(results, &matches))
	rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
	rtest.Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", datafile)
	rtest.Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)

	results = testRunFind(t, true, FindOptions{TreeID: true}, env.gopts, snapshot.Tree.String())
	rtest.OK(t, json.Unmarshal(results, &matches))
	rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", matches)
	rtest.Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", matches[0].Matches)
	rtest.Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)
}

func TestFindSorting(t *testing.T) {
	env, cleanup := withTestEnvironment(t)
	defer cleanup()

	testSetupBackupData(t, env)
	opts := BackupOptions{}

	// first backup
	testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
	sn1 := testListSnapshots(t, env.gopts, 1)[0]

	// second backup
	testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
	snapshots := testListSnapshots(t, env.gopts, 2)
	// get id of new snapshot without depending on file order returned by filesystem
	sn2 := snapshots[0]
	if sn1.Equal(sn2) {
		sn2 = snapshots[1]
	}

	// first restic find - with default FindOptions{}
	results := testRunFind(t, true, FindOptions{}, env.gopts, "testfile")
	lines := strings.Split(string(results), "\n")
	rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", len(lines))
	matches := []testMatches{}
	rtest.OK(t, json.Unmarshal(results, &matches))

	// run second restic find with --reverse, sort oldest to newest
	resultsReverse := testRunFind(t, true, FindOptions{Reverse: true}, env.gopts, "testfile")
	lines = strings.Split(string(resultsReverse), "\n")
	rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", len(lines))
	matchesReverse := []testMatches{}
	rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse))

	// compare result sets
	rtest.Assert(t, sn1.String() == matchesReverse[0].SnapshotID, "snapshot[0] must match old snapshot")
	rtest.Assert(t, sn2.String() == matchesReverse[1].SnapshotID, "snapshot[1] must match new snapshot")
	rtest.Assert(t, matches[0].SnapshotID == matchesReverse[1].SnapshotID, "matches should be sorted 1")
	rtest.Assert(t, matches[1].SnapshotID == matchesReverse[0].SnapshotID, "matches should be sorted 2")
}

func TestFindInvalidTimeRange(t *testing.T) {
	env, cleanup := withTestEnvironment(t)
	defer cleanup()

	err := runFind(context.TODO(), FindOptions{Oldest: "2026-01-01", Newest: "2020-01-01"}, env.gopts, []string{"quack"}, env.gopts.Term)
	rtest.Assert(t, err != nil && err.Error() == "Fatal: --oldest must specify a time before --newest",
		"unexpected error message: %v", err)
}

// JsonOutput is the struct `restic find --json` produces
type JSONOutput struct {
	ObjectType string    `json:"object_type"`
	ID         string    `json:"id"`
	Path       string    `json:"path"`
	ParentTree string    `json:"parent_tree,omitempty"`
	SnapshotID string    `json:"snapshot"`
	Time       time.Time `json:"time,omitempty"`
}

func TestFindPackfile(t *testing.T) {
	env, cleanup := withTestEnvironment(t)
	defer cleanup()

	testSetupBackupData(t, env)

	// backup
	backupPath := env.testdata + "/0/0/9"
	testRunBackup(t, "", []string{backupPath}, BackupOptions{}, env.gopts)
	sn1 := testListSnapshots(t, env.gopts, 1)[0]

	// do all the testing wrapped inside withTermStatus()
	err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
		printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
		_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
		rtest.OK(t, err)
		defer unlock()

		// load master index
		rtest.OK(t, repo.LoadIndex(ctx, printer))

		packID := restic.ID{}
		done := false
		err = repo.ListBlobs(ctx, func(pb restic.PackedBlob) {
			if !done && pb.Type == restic.TreeBlob {
				packID = pb.PackID
				done = true
			}
		})

		rtest.OK(t, err)
		rtest.Assert(t, !packID.IsNull(), "expected a tree packfile ID")
		findOptions := FindOptions{PackID: true}
		results := testRunFind(t, true, findOptions, env.gopts, packID.String())

		// get the json records
		jsonResult := []JSONOutput{}
		rtest.OK(t, json.Unmarshal(results, &jsonResult))
		rtest.Assert(t, len(jsonResult) > 0, "expected at least one tree record in the packfile")

		// look at the last record
		lastIndex := len(jsonResult) - 1
		record := jsonResult[lastIndex]
		rtest.Assert(t, record.ObjectType == "tree" && record.SnapshotID == sn1.String(),
			"expected a tree record with known snapshot id, but got type=%s and snapID=%s instead of %s",
			record.ObjectType, record.SnapshotID, sn1.String())
		backupPath = filepath.ToSlash(backupPath)[2:] // take the offending drive mapping away
		rtest.Assert(t, strings.Contains(record.Path, backupPath), "expected %q as part of %q", backupPath, record.Path)

		return nil
	})
	rtest.OK(t, err)
}

func TestFindPackID(t *testing.T) {
	env, cleanup := withTestEnvironment(t)
	defer cleanup()
	testSetupBackupData(t, env)

	dir009 := filepath.Join(env.testdata, "0", "0", "9")
	dirEntries, err := os.ReadDir(dir009)
	rtest.OK(t, err)
	numberOfFiles := len(dirEntries)

	// backup
	testRunBackup(t, "", []string{dir009}, BackupOptions{}, env.gopts)
	sn1 := testListSnapshots(t, env.gopts, 1)[0]

	// extract packfile ID from repository index
	dataPackID := restic.ID{}
	treePackID := restic.ID{}
	err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
		printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
		_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
		rtest.OK(t, err)
		defer unlock()

		// load Index
		rtest.OK(t, repo.LoadIndex(ctx, nil))
		// go through all index entries and collect data and tree packfile(s)
		rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
			switch blob.Type {
			case restic.DataBlob:
				dataPackID = blob.PackID
			case restic.TreeBlob:
				treePackID = blob.PackID
			}
		}))
		return nil
	})
	rtest.OK(t, err)

	// look for data packfile
	rtest.Assert(t, !dataPackID.IsNull(), "expected to find data packfile in repo")
	packID := dataPackID.String()
	out := testRunFind(t, true, FindOptions{PackID: true}, env.gopts, packID)

	findRes := []JSONOutput{}
	rtest.OK(t, json.Unmarshal(out, &findRes))
	rtest.Assert(t, len(findRes) == numberOfFiles, "expected %d entries for this packfile, got %d",
		numberOfFiles, len(findRes))

	// look for tree packfile
	rtest.Assert(t, !treePackID.IsNull(), "expected to find tree packfile in repo")
	packID = treePackID.String()
	out = testRunFind(t, true, FindOptions{PackID: true}, env.gopts, packID)

	findRes = []JSONOutput{}
	rtest.OK(t, json.Unmarshal(out, &findRes))
	record := findRes[len(findRes)-1]

	rtest.Equals(t, record.ObjectType, "tree")
	rtest.Equals(t, record.SnapshotID, sn1.String())
	// windows path are messy, so we get rid of the messy bits at the start
	// exp: "/C/Users/RUNNER~1/AppData/Local/Temp/restic-test-2921201257/testdata/0/0/9"
	// got: "C:/Users/RUNNER~1/AppData/Local/Temp/restic-test-2921201257/testdata/0/0/9"
	rtest.Equals(t, filepath.ToSlash(record.Path)[2:], filepath.ToSlash(dir009)[2:])
}
