// 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"
	"io/ioutil"
	"os"
	"testing"
	"time"

	"github.com/syncthing/syncthing/lib/config"
	"github.com/syncthing/syncthing/lib/db"
	"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.

	if err := os.RemoveAll("_recvonly"); err != nil && !os.IsNotExist(err) {
		t.Fatal(err)
	}
	defer func() {
		if err := os.RemoveAll("_recvonly"); err != nil && !os.IsNotExist(err) {
			t.Fatal(err)
		}
	}()

	// Create some test data

	if err := os.MkdirAll("_recvonly/.stfolder", 0755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll("_recvonly/ignDir", 0755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll("_recvonly/unknownDir", 0755); err != nil {
		t.Fatal(err)
	}
	if err := ioutil.WriteFile("_recvonly/ignDir/ignFile", []byte("hello\n"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := ioutil.WriteFile("_recvonly/unknownDir/unknownFile", []byte("hello\n"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := ioutil.WriteFile("_recvonly/.stignore", []byte("ignDir\n"), 0644); err != nil {
		t.Fatal(err)
	}

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

	// Get us a model up and running

	m := setupROFolder()
	defer m.Stop()

	// Send and index update for the known stuff

	m.Index(device1, "ro", knownFiles)
	m.updateLocalsFromScanning("ro", knownFiles)

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

	// Start the folder. This will cause a scan, should discover the other stuff in the folder

	m.StartFolder("ro")
	m.ScanFolder("ro")

	// We should now have two files and two directories.

	size = m.GlobalSize("ro")
	if size.Files != 2 || size.Directories != 2 {
		t.Fatalf("Global: expected 2 files and 2 directories: %+v", size)
	}
	size = m.LocalSize("ro")
	if size.Files != 2 || size.Directories != 2 {
		t.Fatalf("Local: expected 2 files and 2 directories: %+v", size)
	}
	size = m.ReceiveOnlyChangedSize("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{"_recvonly/knownDir/knownFile", "_recvonly/ignDir/ignFile"} {
		_, err := os.Stat(p)
		if err != nil {
			t.Error("Unexpected error:", err)
		}
	}

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

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

	size = m.GlobalSize("ro")
	if size.Files != 1 || size.Directories != 1 {
		t.Fatalf("Global: expected 1 files and 1 directories: %+v", size)
	}
	size = m.LocalSize("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.

	if err := os.RemoveAll("_recvonly"); err != nil && !os.IsNotExist(err) {
		t.Fatal(err)
	}
	defer func() {
		if err := os.RemoveAll("_recvonly"); err != nil && !os.IsNotExist(err) {
			t.Fatal(err)
		}
	}()

	// Create some test data

	if err := os.MkdirAll("_recvonly/.stfolder", 0755); err != nil {
		t.Fatal(err)
	}
	oldData := []byte("hello\n")
	knownFiles := setupKnownFiles(t, oldData)

	// Get us a model up and running

	m := setupROFolder()
	defer m.Stop()

	// Send and index update for the known stuff

	m.Index(device1, "ro", knownFiles)
	m.updateLocalsFromScanning("ro", knownFiles)

	// Start the folder. This will cause a scan.

	m.StartFolder("ro")
	m.ScanFolder("ro")

	// Everything should be in sync.

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

	// Update the file.

	newData := []byte("totally different data\n")
	if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", newData, 0644); err != nil {
		t.Fatal(err)
	}

	// Rescan.

	if err := m.ScanFolder("ro"); err != nil {
		t.Fatal(err)
	}

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

	size = m.GlobalSize("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 = m.LocalSize("ro")
	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
		t.Fatalf("Local: expected the new file to be reflected: %+v", size)
	}
	size = m.NeedSize("ro")
	if size.Files+size.Directories > 0 {
		t.Fatalf("Need: expected nothing: %+v", size)
	}
	size = m.ReceiveOnlyChangedSize("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 = m.GlobalSize("ro")
	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
		t.Fatalf("Global: expected the global size to revert: %+v", size)
	}
	size = m.LocalSize("ro")
	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
		t.Fatalf("Local: expected the local size to remain: %+v", size)
	}
	size = m.NeedSize("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) {
	if err := os.RemoveAll("_recvonly"); err != nil && !os.IsNotExist(err) {
		t.Fatal(err)
	}
	defer func() {
		if err := os.RemoveAll("_recvonly"); err != nil && !os.IsNotExist(err) {
			t.Fatal(err)
		}
	}()

	// Create some test data

	if err := os.MkdirAll("_recvonly/.stfolder", 0755); err != nil {
		t.Fatal(err)
	}
	oldData := []byte("hello\n")
	knownFiles := setupKnownFiles(t, oldData)

	// Get us a model up and running

	m := setupROFolder()
	defer m.Stop()

	m.fmut.Lock()
	fset := m.folderFiles["ro"]
	m.fmut.Unlock()
	folderFs := fset.MtimeFS()

	// Send and index update for the known stuff

	m.Index(device1, "ro", knownFiles)
	m.updateLocalsFromScanning("ro", knownFiles)

	// Start the folder. This will cause a scan.

	m.StartFolder("ro")
	m.ScanFolder("ro")

	// Everything should be in sync.

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

	// Create a file and modify another

	file := "_recvonly/foo"
	if err := ioutil.WriteFile(file, []byte("hello\n"), 0644); err != nil {
		t.Fatal(err)
	}

	if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", []byte("bye\n"), 0644); err != nil {
		t.Fatal(err)
	}

	m.ScanFolder("ro")

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

	// Remove the file again and undo the modification

	if err := os.Remove(file); err != nil {
		t.Fatal(err)
	}
	if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", oldData, 0644); err != nil {
		t.Fatal(err)
	}
	folderFs.Chtimes("knownDir/knownFile", knownFiles[1].ModTime(), knownFiles[1].ModTime())

	m.ScanFolder("ro")

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

func setupKnownFiles(t *testing.T, data []byte) []protocol.FileInfo {
	if err := os.MkdirAll("_recvonly/knownDir", 0755); err != nil {
		t.Fatal(err)
	}
	if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", data, 0644); err != nil {
		t.Fatal(err)
	}

	t0 := time.Now().Add(-1 * time.Minute)
	if err := os.Chtimes("_recvonly/knownDir/knownFile", t0, t0); err != nil {
		t.Fatal(err)
	}

	fi, err := os.Stat("_recvonly/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: 0755,
			Version:     protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 42}}},
			Sequence:    42,
		},
		{
			Name:        "knownDir/knownFile",
			Type:        protocol.FileInfoTypeFile,
			Permissions: 0644,
			Size:        fi.Size(),
			ModifiedS:   fi.ModTime().Unix(),
			ModifiedNs:  int32(fi.ModTime().UnixNano() % 1e9),
			Version:     protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 42}}},
			Sequence:    42,
			Blocks:      blocks,
		},
	}

	return knownFiles
}

func setupROFolder() *Model {
	fcfg := config.NewFolderConfiguration(protocol.LocalDeviceID, "ro", "receive only test", fs.FilesystemTypeBasic, "_recvonly")
	fcfg.Type = config.FolderTypeReceiveOnly
	fcfg.Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
	fcfg.FSWatcherEnabled = false
	fcfg.RescanIntervalS = 86400

	cfg := defaultCfg.Copy()
	cfg.Folders = append(cfg.Folders, fcfg)

	wrp := config.Wrap("/dev/null", cfg)

	db := db.OpenMemory()
	m := NewModel(wrp, protocol.LocalDeviceID, "syncthing", "dev", db, nil)

	m.ServeBackground()
	m.AddFolder(fcfg)

	return m
}
