// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

package model

import (
	"bytes"
	"context"
	"path/filepath"
	"testing"
	"time"

	"github.com/syncthing/syncthing/lib/config"
	"github.com/syncthing/syncthing/lib/events"
	"github.com/syncthing/syncthing/lib/fs"
	"github.com/syncthing/syncthing/lib/protocol"
	"github.com/syncthing/syncthing/lib/scanner"
)

func TestRecvOnlyRevertDeletes(t *testing.T) {
	// Make sure that we delete extraneous files and directories when we hit
	// Revert.

	// Get us a model up and running

	m, f, wcfgCancel := setupROFolder(t)
	defer wcfgCancel()
	ffs := f.Filesystem(nil)
	defer cleanupModel(m)
	addFakeConn(m, device1, f.ID)

	// Create some test data

	for _, dir := range []string{".stfolder", "ignDir", "unknownDir"} {
		must(t, ffs.MkdirAll(dir, 0o755))
	}
	writeFilePerm(t, ffs, "ignDir/ignFile", []byte("hello\n"), 0o644)
	writeFilePerm(t, ffs, "unknownDir/unknownFile", []byte("hello\n"), 0o644)
	writeFilePerm(t, ffs, ".stignore", []byte("ignDir\n"), 0o644)

	knownFiles := setupKnownFiles(t, ffs, []byte("hello\n"))

	// Send and index update for the known stuff

	must(t, m.Index(device1, "ro", knownFiles))
	f.updateLocalsFromScanning(knownFiles)

	size := globalSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
	}

	// Scan, should discover the other stuff in the folder

	must(t, m.ScanFolder("ro"))

	// We should now have two files and two directories, with global state unchanged.

	size = globalSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Global: expected 2 files and 2 directories: %+v", size)
	}
	size = localSize(t, m, "ro")
	if size.Files != 2 || size.Directories != 2 {
		t.Fatalf("Local: expected 2 files and 2 directories: %+v", size)
	}
	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories == 0 {
		t.Fatalf("ROChanged: expected something: %+v", size)
	}

	// Revert should delete the unknown stuff

	m.Revert("ro")

	// These should still exist
	for _, p := range []string{"knownDir/knownFile", "ignDir/ignFile"} {
		if _, err := ffs.Stat(p); err != nil {
			t.Error("Unexpected error:", err)
		}
	}

	// These should have been removed
	for _, p := range []string{"unknownDir", "unknownDir/unknownFile"} {
		if _, err := ffs.Stat(p); !fs.IsNotExist(err) {
			t.Error("Unexpected existing thing:", p)
		}
	}

	// We should now have one file and directory again.

	size = globalSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Global: expected 1 files and 1 directories: %+v", size)
	}
	size = localSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Local: expected 1 files and 1 directories: %+v", size)
	}
}

func TestRecvOnlyRevertNeeds(t *testing.T) {
	// Make sure that a new file gets picked up and considered latest, then
	// gets considered old when we hit Revert.

	// Get us a model up and running

	m, f, wcfgCancel := setupROFolder(t)
	defer wcfgCancel()
	ffs := f.Filesystem(nil)
	defer cleanupModel(m)
	addFakeConn(m, device1, f.ID)

	// Create some test data

	must(t, ffs.MkdirAll(".stfolder", 0o755))
	oldData := []byte("hello\n")
	knownFiles := setupKnownFiles(t, ffs, oldData)

	// Send and index update for the known stuff

	must(t, m.Index(device1, "ro", knownFiles))
	f.updateLocalsFromScanning(knownFiles)

	// Scan the folder.

	must(t, m.ScanFolder("ro"))

	// Everything should be in sync.

	size := globalSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
	}
	size = localSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
	}
	size = needSizeLocal(t, m, "ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("Need: expected nothing: %+v", size)
	}
	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("ROChanged: expected nothing: %+v", size)
	}

	// Update the file.

	newData := []byte("totally different data\n")
	writeFilePerm(t, ffs, "knownDir/knownFile", newData, 0o644)

	// Rescan.

	must(t, m.ScanFolder("ro"))

	// We now have a newer file than the rest of the cluster. Global state should reflect this.

	size = globalSize(t, m, "ro")
	const sizeOfDir = 128
	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
		t.Fatalf("Global: expected no change due to the new file: %+v", size)
	}
	size = localSize(t, m, "ro")
	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
		t.Fatalf("Local: expected the new file to be reflected: %+v", size)
	}
	size = needSizeLocal(t, m, "ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("Need: expected nothing: %+v", size)
	}
	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories == 0 {
		t.Fatalf("ROChanged: expected something: %+v", size)
	}

	// We hit the Revert button. The file that was new should become old.

	m.Revert("ro")

	size = globalSize(t, m, "ro")
	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
		t.Fatalf("Global: expected the global size to revert: %+v", size)
	}
	size = localSize(t, m, "ro")
	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
		t.Fatalf("Local: expected the local size to remain: %+v", size)
	}
	size = needSizeLocal(t, m, "ro")
	if size.Files != 1 || size.Bytes != int64(len(oldData)) {
		t.Fatalf("Local: expected to need the old file data: %+v", size)
	}
}

func TestRecvOnlyUndoChanges(t *testing.T) {
	// Get us a model up and running

	m, f, wcfgCancel := setupROFolder(t)
	defer wcfgCancel()
	ffs := f.Filesystem(nil)
	defer cleanupModel(m)
	addFakeConn(m, device1, f.ID)

	// Create some test data

	must(t, ffs.MkdirAll(".stfolder", 0o755))
	oldData := []byte("hello\n")
	knownFiles := setupKnownFiles(t, ffs, oldData)

	// Send an index update for the known stuff

	must(t, m.Index(device1, "ro", knownFiles))
	f.updateLocalsFromScanning(knownFiles)

	// Scan the folder.

	must(t, m.ScanFolder("ro"))

	// Everything should be in sync.

	size := globalSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
	}
	size = localSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
	}
	size = needSizeLocal(t, m, "ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("Need: expected nothing: %+v", size)
	}
	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("ROChanged: expected nothing: %+v", size)
	}

	// Create a file and modify another

	const file = "foo"
	writeFilePerm(t, ffs, file, []byte("hello\n"), 0o644)
	writeFilePerm(t, ffs, "knownDir/knownFile", []byte("bye\n"), 0o644)

	must(t, m.ScanFolder("ro"))

	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files != 2 {
		t.Fatalf("Receive only: expected 2 files: %+v", size)
	}

	// Remove the file again and undo the modification

	must(t, ffs.Remove(file))
	writeFilePerm(t, ffs, "knownDir/knownFile", oldData, 0o644)
	must(t, ffs.Chtimes("knownDir/knownFile", knownFiles[1].ModTime(), knownFiles[1].ModTime()))

	must(t, m.ScanFolder("ro"))

	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories+size.Deleted != 0 {
		t.Fatalf("Receive only: expected all zero: %+v", size)
	}
}

func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
	// Get us a model up and running

	m, f, wcfgCancel := setupROFolder(t)
	defer wcfgCancel()
	ffs := f.Filesystem(nil)
	defer cleanupModel(m)
	addFakeConn(m, device1, f.ID)

	// Create some test data

	must(t, ffs.MkdirAll(".stfolder", 0o755))
	oldData := []byte("hello\n")
	knownFiles := setupKnownFiles(t, ffs, oldData)

	// Send an index update for the known stuff

	must(t, m.Index(device1, "ro", knownFiles))
	f.updateLocalsFromScanning(knownFiles)

	// Scan the folder.

	must(t, m.ScanFolder("ro"))

	// Everything should be in sync.

	size := globalSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
	}
	size = localSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
	}
	size = needSizeLocal(t, m, "ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("Need: expected nothing: %+v", size)
	}
	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("ROChanged: expected nothing: %+v", size)
	}

	// Delete our file

	must(t, ffs.Remove(knownFiles[1].Name))

	must(t, m.ScanFolder("ro"))

	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Deleted != 1 {
		t.Fatalf("Receive only: expected 1 deleted: %+v", size)
	}

	// Drop the remote

	f.fset.Drop(device1)
	must(t, m.ScanFolder("ro"))

	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Deleted != 0 {
		t.Fatalf("Receive only: expected no deleted: %+v", size)
	}
}

func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
	// Get us a model up and running

	m, f, wcfgCancel := setupROFolder(t)
	defer wcfgCancel()
	ffs := f.Filesystem(nil)
	defer cleanupModel(m)
	addFakeConn(m, device1, f.ID)

	// Create some test data

	must(t, ffs.MkdirAll(".stfolder", 0o755))
	oldData := []byte("hello\n")
	knownFiles := setupKnownFiles(t, ffs, oldData)

	// Send an index update for the known stuff

	must(t, m.Index(device1, "ro", knownFiles))
	f.updateLocalsFromScanning(knownFiles)

	// Scan the folder.

	must(t, m.ScanFolder("ro"))

	// Everything should be in sync.

	size := globalSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
	}
	size = localSize(t, m, "ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
	}
	size = needSizeLocal(t, m, "ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("Need: expected nothing: %+v", size)
	}
	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("ROChanged: expected nothing: %+v", size)
	}

	// Create a file and modify another

	const file = "foo"
	knownFile := filepath.Join("knownDir", "knownFile")
	writeFilePerm(t, ffs, file, []byte("hello\n"), 0o644)
	writeFilePerm(t, ffs, knownFile, []byte("bye\n"), 0o644)

	must(t, m.ScanFolder("ro"))

	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files != 2 {
		t.Fatalf("Receive only: expected 2 files: %+v", size)
	}

	// Do the same changes on the remote

	files := make([]protocol.FileInfo, 0, 2)
	snap := fsetSnapshot(t, f.fset)
	snap.WithHave(protocol.LocalDeviceID, func(fi protocol.FileIntf) bool {
		if n := fi.FileName(); n != file && n != knownFile {
			return true
		}
		f := fi.(protocol.FileInfo)
		f.LocalFlags = 0
		f.Version = protocol.Vector{}.Update(device1.Short())
		files = append(files, f)
		return true
	})
	snap.Release()
	must(t, m.IndexUpdate(device1, "ro", files))

	// Ensure the pull to resolve conflicts (content identical) happened
	must(t, f.doInSync(func() error {
		f.pull()
		return nil
	}))

	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories+size.Deleted != 0 {
		t.Fatalf("Receive only: expected all zero: %+v", size)
	}
}

func TestRecvOnlyRevertOwnID(t *testing.T) {
	// If the folder was receive-only in the past, the global item might have
	// only our id in the version vector and be valid. There was a bug based on
	// the incorrect assumption that this can never happen.

	// Get us a model up and running

	m, f, wcfgCancel := setupROFolder(t)
	defer wcfgCancel()
	ffs := f.Filesystem(nil)
	defer cleanupModel(m)
	addFakeConn(m, device1, f.ID)

	// Create some test data

	must(t, ffs.MkdirAll(".stfolder", 0o755))
	data := []byte("hello\n")
	name := "foo"
	writeFilePerm(t, ffs, name, data, 0o644)

	// Make sure the file is scanned and locally changed
	must(t, m.ScanFolder("ro"))
	fi, ok := m.testCurrentFolderFile(f.ID, name)
	if !ok {
		t.Fatal("File missing")
	} else if !fi.IsReceiveOnlyChanged() {
		t.Fatal("File should be receiveonly changed")
	}
	fi.LocalFlags = 0
	v := fi.Version.Counters[0].Value
	fi.Version.Counters[0].Value = uint64(time.Unix(int64(v), 0).Add(-10 * time.Second).Unix())

	// Monitor the outcome
	sub := f.evLogger.Subscribe(events.LocalIndexUpdated)
	defer sub.Unsubscribe()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case <-sub.C():
				if file, _ := m.testCurrentFolderFile(f.ID, name); file.Deleted {
					t.Error("local file was deleted")
					cancel()
				} else if file.IsEquivalent(fi, f.modTimeWindow) {
					cancel() // That's what we are waiting for
				}
			}
		}
	}()

	// Receive an index update with an older version, but valid and then revert
	must(t, m.Index(device1, f.ID, []protocol.FileInfo{fi}))
	f.Revert()

	select {
	case <-ctx.Done():
	case <-time.After(10 * time.Second):
		t.Fatal("timed out")
	}
}

func setupKnownFiles(t *testing.T, ffs fs.Filesystem, data []byte) []protocol.FileInfo {
	t.Helper()

	must(t, ffs.MkdirAll("knownDir", 0o755))
	writeFilePerm(t, ffs, "knownDir/knownFile", data, 0o644)

	t0 := time.Now().Add(-1 * time.Minute)
	must(t, ffs.Chtimes("knownDir/knownFile", t0, t0))

	fi, err := ffs.Stat("knownDir/knownFile")
	if err != nil {
		t.Fatal(err)
	}
	blocks, _ := scanner.Blocks(context.TODO(), bytes.NewReader(data), protocol.BlockSize(int64(len(data))), int64(len(data)), nil, true)
	knownFiles := []protocol.FileInfo{
		{
			Name:        "knownDir",
			Type:        protocol.FileInfoTypeDirectory,
			Permissions: 0o755,
			Version:     protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 42}}},
			Sequence:    42,
		},
		{
			Name:        "knownDir/knownFile",
			Type:        protocol.FileInfoTypeFile,
			Permissions: 0o644,
			Size:        fi.Size(),
			ModifiedS:   fi.ModTime().Unix(),
			ModifiedNs:  int(fi.ModTime().UnixNano() % 1e9),
			Version:     protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 42}}},
			Sequence:    42,
			Blocks:      blocks,
		},
	}

	return knownFiles
}

func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder, context.CancelFunc) {
	t.Helper()

	w, cancel := newConfigWrapper(defaultCfg)
	cfg := w.RawCopy()
	fcfg := newFolderConfig()
	fcfg.ID = "ro"
	fcfg.Label = "ro"
	fcfg.Type = config.FolderTypeReceiveOnly
	cfg.Folders = []config.FolderConfiguration{fcfg}
	replace(t, w, cfg)

	m := newModel(t, w, myID, "syncthing", "dev", nil)
	m.ServeBackground()
	<-m.started
	must(t, m.ScanFolder("ro"))

	m.fmut.RLock()
	defer m.fmut.RUnlock()
	f := m.folderRunners["ro"].(*receiveOnlyFolder)

	return m, f, cancel
}
