// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pilosa

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/pilosa/pilosa/v2/rbf"
	"github.com/pilosa/pilosa/v2/shardwidth"
	txkey "github.com/pilosa/pilosa/v2/short_txkey"
	//txkey "github.com/pilosa/pilosa/v2/txkey"
)

// Shard per db evaluation
func TestShardPerDB_SetBit(t *testing.T) {
	f, idx, tx := mustOpenFragment(t, "i", "f", viewStandard, 0, "")
	_ = idx
	defer f.Clean(t)

	// Set bits on the fragment.
	if _, err := f.setBit(tx, 120, 1); err != nil {
		t.Fatal(err)
	} else if _, err := f.setBit(tx, 120, 6); err != nil {
		t.Fatal(err)
	} else if _, err := f.setBit(tx, 121, 0); err != nil {
		t.Fatal(err)
	}
	// should have two containers set in the fragment.

	// Verify counts on rows.
	if n := f.mustRow(tx, 120).Count(); n != 2 {
		t.Fatalf("unexpected count: %d", n)
	} else if n := f.mustRow(tx, 121).Count(); n != 1 {
		t.Fatalf("unexpected count: %d", n)
	}

	// commit the change, and verify it is still there
	panicOn(tx.Commit())

	// Close and reopen the fragment & verify the data.
	err := f.Reopen() // roaring data not being flushed? red on roaring
	if err != nil {
		t.Fatal(err)
	}
	tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard})
	defer tx.Rollback()

	if n := f.mustRow(tx, 120).Count(); n != 2 {
		t.Fatalf("unexpected count (reopen): %d", n)
	} else if n := f.mustRow(tx, 121).Count(); n != 1 {
		t.Fatalf("unexpected count (reopen): %d", n)
	}
}

// test that we find all *local* shards
func Test_DBPerShard_GetShardsForIndex_LocalOnly(t *testing.T) {

	tmpdir, err := ioutil.TempDir("", "Test_DBPerShard_GetShardsForIndex_LocalOnly")
	panicOn(err)

	orig := os.Getenv("PILOSA_TXSRC")
	defer os.Setenv("PILOSA_TXSRC", orig) // must restore or will mess up other tests!

	v2s := NewFieldView2Shards()
	stdShardSet := newShardSet()
	for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
		stdShardSet.add(shard)
	}
	for _, field := range []string{"f", "_exists"} {
		v2s.addViewShardSet(txkey.FieldView{Field: field, View: "standard"}, stdShardSet)
	}

	for _, src := range []string{"roaring", "bolt", "rbf"} {

		os.Setenv("PILOSA_TXSRC", src)

		// must make Holder AFTER setting src.
		holder := NewHolder(tmpdir, nil)

		index := "rick"
		idx := makeSampleRoaringDir(tmpdir, index, src, 1, holder, v2s)
		if idx == nil {
			idx, err = NewIndex(holder, filepath.Join(tmpdir, index), index)
			panicOn(err)
		}
		estd := "rick/_exists/views/standard"
		std := "rick/f/views/standard"

		shards, err := holder.txf.GetShardsForIndex(idx, tmpdir+sep+std, false)
		panicOn(err)

		for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
			if !shards[shard] {
				panic(fmt.Sprintf("missing shard=%v from shards='%#v'", shard, shards))
			}
		}
		if src == "roaring" {
			// check estd too
			shards, err = holder.txf.GetShardsForIndex(idx, tmpdir+sep+estd, false)
			panicOn(err)
			for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
				if !shards[shard] {
					panic(fmt.Sprintf("missing shard=%v from shards='%#v'", shard, shards))
				}
			}

			// check GetSortedFieldViewList() and roaringGetFieldView2Shards()
			vs, err := roaringGetFieldView2Shards(idx)
			panicOn(err)

			for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
				tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Shard: shard})
				fvs, err := tx.GetSortedFieldViewList(idx, shard)
				panicOn(err)
				// expect these same two field/views for all 6 shards
				expect0 := txkey.FieldView{Field: "_exists", View: "standard"}
				expect1 := txkey.FieldView{Field: "f", View: "standard"}
				if len(fvs) != 2 {
					panic(fmt.Sprintf("fvs should be len 2, got '%#v'", fvs))
				}
				if fvs[0] != expect0 {
					panic(fmt.Sprintf("expected fvs[0]='%#v', but got '%#v'", expect0, fvs[0]))
				}
				if fvs[1] != expect1 {
					panic(fmt.Sprintf("expected fvs[1]='%#v', but got '%#v'", expect1, fvs[1]))
				}

				for _, fv := range fvs {
					if !vs.has(fv.Field, fv.View, shard) {
						panic(fmt.Sprintf("vs did not contain fv='%#v' for shard %v", fv, shard))
					}
				}
				tx.Rollback()
			}
		} else {
			// non-roaring: rbf, bolt

			for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
				tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Shard: shard})
				fvs, err := tx.GetSortedFieldViewList(idx, shard)
				panicOn(err)
				// expect these same two field/views for all 6 shards
				expect0 := txkey.FieldView{Field: "_exists", View: "standard"}
				expect1 := txkey.FieldView{Field: "f", View: "standard"}
				if len(fvs) != 2 {
					panic(fmt.Sprintf("fvs should be len 2, got '%#v'", fvs))
				}
				if fvs[0] != expect0 {
					panic(fmt.Sprintf("expected fvs[0]='%#v', but got '%#v'", expect0, fvs[0]))
				}
				if fvs[1] != expect1 {
					panic(fmt.Sprintf("expected fvs[1]='%#v', but got '%#v'", expect1, fvs[1]))
				}
				tx.Rollback()
			}
		}
		holder.Close()
	}
}

// data for Test_DBPerShard_GetShardsForIndex
//
var sampleRoaringDirList = map[string]string{"roaring": `
rick/f/views/standard/fragments/215.cache
rick/f/views/standard/fragments/221.cache
rick/f/views/standard/fragments/223.cache
rick/f/views/standard/fragments/93.cache
rick/f/views/standard/fragments/217.cache
rick/f/views/standard/fragments/219.cache
rick/f/views/standard/fragments/217
rick/f/views/standard/fragments/219
rick/f/views/standard/fragments/215
rick/f/views/standard/fragments/221
rick/f/views/standard/fragments/223
rick/f/views/standard/fragments/93
rick/_exists/views/standard/fragments/221
rick/_exists/views/standard/fragments/215
rick/_exists/views/standard/fragments/217
rick/_exists/views/standard/fragments/93
rick/_exists/views/standard/fragments/219
rick/_exists/views/standard/fragments/223
`,
	"bolt": `
rick.index.txstores@@@/store-boltdb@@/shard.0093-boltdb@/bolt.db
rick.index.txstores@@@/store-boltdb@@/shard.0215-boltdb@/bolt.db
rick.index.txstores@@@/store-boltdb@@/shard.0217-boltdb@/bolt.db
rick.index.txstores@@@/store-boltdb@@/shard.0219-boltdb@/bolt.db
rick.index.txstores@@@/store-boltdb@@/shard.0221-boltdb@/bolt.db
rick.index.txstores@@@/store-boltdb@@/shard.0223-boltdb@/bolt.db
`,
	"rbf": `
rick.index.txstores@@@/store-rbfdb@@/shard.0093-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0215-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0217-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0219-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0221-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0223-rbfdb@
`,
}

func makeSampleRoaringDir(root, index, txsrc string, minBytes int, h *Holder, view2shards *FieldView2Shards) (idx *Index) {

	shards := []uint64{0, 93, 215, 217, 219, 221, 223}
	fns := strings.Split(sampleRoaringDirList[txsrc], "\n")
	firstDone := false

	for i, fn := range fns {
		if fn == "" {
			continue
		}
		var shard uint64
		if txsrc != "roaring" {
			// only have shards for the non-roaring
			shard = shards[i]
		}
		switch txsrc {
		case "bolt", "rbf":
			idx = helperCreateDBShard(h, index, shard)

			// first time only, we'll actually make all the shards at this point because
			// view2shards has them all anyway.
			if !firstDone {
				firstDone = true
				makeTxTestDBWithViewsShards(h, idx, view2shards)
			}
			continue
		}

		path := root + sep + filepath.Dir(fn)
		panicOn(os.MkdirAll(path, 0755))
		fd, err := os.Create(root + sep + fn)
		panicOn(err)
		if minBytes > 0 {
			_, err := fd.Write(make([]byte, minBytes))
			panicOn(err)
		}
		fd.Close()
	}
	return
}

func helperCreateDBShard(h *Holder, index string, shard uint64) *Index {
	idx, err := h.CreateIndexIfNotExists(index, IndexOptions{})
	panicOn(err)
	dbs, err := h.txf.dbPerShard.GetDBShard(index, shard, idx)
	panicOn(err)
	_ = dbs
	return idx
}

// keep the ocd linter happy
var _ = makeBolttestDB
var _ = makeRBFtestDB

func makeBolttestDB(path string, h *Holder, shard uint64) {
	i := uint64(1)
	w, _ := mustOpenEmptyBoltWrapper(path)
	BoltMustSetBitvalue(w, "index", "field", "view", shard, i)
	w.Close()
}

func makeRBFtestDB(path string, h *Holder, shard uint64) {
	i := uint64(1)

	db := rbf.NewDB(path, nil)
	err := db.Open()
	panicOn(err)
	defer db.Close()

	tx, err := db.Begin(true)
	panicOn(err)

	err = tx.CreateBitmap("x")
	panicOn(err)

	_, err = tx.Add("x", i)
	panicOn(err)

	err = tx.Commit()
	panicOn(err)
}

func makeTxTestDBWithViewsShards(holder *Holder, idx *Index, exp *FieldView2Shards) {

	// TODO(jea): need date time quantum views!!
	for field, viewmap := range exp.m {
		for view, shset := range viewmap {

			ss := shset.CloneMaybe()
			for shard := range ss {

				// simply write 1 bit to each shard to force its creation.
				bits := []uint64{(shard << shardwidth.Exponent) + 1}
				tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard})
				changeCount, err := tx.Add(idx.name, field, view, shard, bits...)
				panicOn(err)
				if changeCount != len(bits) {
					panic(fmt.Sprintf("writing field '%v', view '%v' shard '%v', expected changeCount to equal len bits = %v but was %v", field, view, shard, len(bits), changeCount))
				}

				panicOn(tx.Commit())
			}
		}
	}

}

// test that rbf can give us a map[view]*shardSet
func Test_DBPerShard_GetFieldView2Shards_map_from_RBF(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "Test_DBPerShard_GetFieldView2Shards_map_from_RBF")
	panicOn(err)

	orig := os.Getenv("PILOSA_TXSRC")
	defer os.Setenv("PILOSA_TXSRC", orig) // must restore or will mess up other tests!

	os.Setenv("PILOSA_TXSRC", "rbf")

	// must make Holder AFTER setting src.
	holder := NewHolder(tmpdir, nil)
	defer holder.Close()

	index := "rick"
	field := "f"
	idx, err := holder.createIndex(index, IndexOptions{})
	panicOn(err)

	exp := NewFieldView2Shards()

	stdShardSet := newShardSet()
	stdShardSet.add(12)
	stdShardSet.add(15)
	exp.addViewShardSet(txkey.FieldView{Field: field, View: "standard"}, stdShardSet)

	hrShardSet := newShardSet()
	hrShardSet.add(7)
	exp.addViewShardSet(txkey.FieldView{Field: field, View: "standard_2019092416"}, hrShardSet)

	makeTxTestDBWithViewsShards(holder, idx, exp)

	// setup is done
	view2shard, err := holder.txf.GetFieldView2ShardsMapForIndex(idx)
	panicOn(err)

	// compare against setup
	if !view2shard.equals(exp) {
		panic(fmt.Sprintf("expected '%v' but got view2shard '%v'", exp, view2shard))
	}
}
