// 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 db

import (
	"encoding/json"
	"io"
	"os"
	"testing"

	"github.com/syncthing/syncthing/lib/db/backend"
	"github.com/syncthing/syncthing/lib/events"
)

// writeJSONS serializes the database to a JSON stream that can be checked
// in to the repo and used for tests.
func writeJSONS(w io.Writer, db backend.Backend) {
	it, err := db.NewPrefixIterator(nil)
	if err != nil {
		panic(err)
	}
	defer it.Release()
	enc := json.NewEncoder(w)
	for it.Next() {
		err := enc.Encode(map[string][]byte{
			"k": it.Key(),
			"v": it.Value(),
		})
		if err != nil {
			panic(err)
		}
	}
}

// we know this function isn't generally used, nonetheless we want it in
// here and the linter to not complain.
var _ = writeJSONS

// openJSONS reads a JSON stream file into a backend DB
func openJSONS(file string) (backend.Backend, error) {
	fd, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	dec := json.NewDecoder(fd)

	db := backend.OpenMemory()

	for {
		var row map[string][]byte

		err := dec.Decode(&row)
		if err == io.EOF {
			break
		} else if err != nil {
			return nil, err
		}

		if err := db.Put(row["k"], row["v"]); err != nil {
			return nil, err
		}
	}

	return db, nil
}

func newLowlevel(t testing.TB, backend backend.Backend) *Lowlevel {
	t.Helper()
	ll, err := NewLowlevel(backend, events.NoopLogger)
	if err != nil {
		t.Fatal(err)
	}
	return ll
}

func newLowlevelMemory(t testing.TB) *Lowlevel {
	return newLowlevel(t, backend.OpenMemory())
}

func newFileSet(t testing.TB, folder string, db *Lowlevel) *FileSet {
	t.Helper()
	fset, err := NewFileSet(folder, db)
	if err != nil {
		t.Fatal(err)
	}
	return fset
}

func snapshot(t testing.TB, fset *FileSet) *Snapshot {
	t.Helper()
	snap, err := fset.Snapshot()
	if err != nil {
		t.Fatal(err)
	}
	return snap
}

// The following commented tests were used to generate jsons files to stdout for
// future tests and are kept here for reference (reuse).

// TestGenerateIgnoredFilesDB generates a database with files with invalid flags,
// local and remote, in the format used in 0.14.48.
// func TestGenerateIgnoredFilesDB(t *testing.T) {
// 	db := OpenMemory()
// 	fs := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
// 	fs.Update(protocol.LocalDeviceID, []protocol.FileInfo{
// 		{ // invalid (ignored) file
// 			Name:    "foo",
// 			Type:    protocol.FileInfoTypeFile,
// 			Invalid: true,
// 			Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 1000}}},
// 		},
// 		{ // regular file
// 			Name:    "bar",
// 			Type:    protocol.FileInfoTypeFile,
// 			Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 1001}}},
// 		},
// 	})
// 	fs.Update(protocol.DeviceID{42}, []protocol.FileInfo{
// 		{ // invalid file
// 			Name:    "baz",
// 			Type:    protocol.FileInfoTypeFile,
// 			Invalid: true,
// 			Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1000}}},
// 		},
// 		{ // regular file
// 			Name:    "quux",
// 			Type:    protocol.FileInfoTypeFile,
// 			Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1002}}},
// 		},
// 	})
// 	writeJSONS(os.Stdout, db.DB)
// }

// TestGenerateUpdate0to3DB generates a database with files with invalid flags, prefixed
// by a slash and other files to test database migration from version 0 to 3, in the
// format used in 0.14.45.
// func TestGenerateUpdate0to3DB(t *testing.T) {
// 	db := OpenMemory()
// 	fs := newFileSet(t, update0to3Folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
// 	for devID, files := range haveUpdate0to3 {
// 		fs.Update(devID, files)
// 	}
// 	writeJSONS(os.Stdout, db.DB)
// }

// func TestGenerateUpdateTo10(t *testing.T) {
// 	db := newLowlevelMemory(t)
// 	defer db.Close()

// 	if err := UpdateSchema(db); err != nil {
// 		t.Fatal(err)
// 	}

// 	fs := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), db)

// 	files := []protocol.FileInfo{
// 		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Deleted: true, Sequence: 1},
// 		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2), Sequence: 2},
// 		{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Deleted: true, Sequence: 3},
// 	}
// 	fs.Update(protocol.LocalDeviceID, files)
// 	files[1].Version = files[1].Version.Update(remoteDevice0.Short())
// 	files[1].Deleted = true
// 	files[2].Version = files[2].Version.Update(remoteDevice0.Short())
// 	files[2].Blocks = genBlocks(1)
// 	files[2].Deleted = false
// 	fs.Update(remoteDevice0, files)

// 	fd, err := os.Create("./testdata/v1.4.0-updateTo10.json")
// 	if err != nil {
// 		panic(err)
// 	}
// 	defer fd.Close()
// 	writeJSONS(fd, db)
// }
