// Copyright (C) 2018-2021  Nexedi SA and Contributors.
//                          Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.

package zodb

import (
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"reflect"
	"runtime"
	"testing"

	"lab.nexedi.com/kirr/neo/go/transaction"

	"lab.nexedi.com/kirr/go123/mem"
	assert "github.com/stretchr/testify/require"
)

// test Persistent type.
type MyObject struct {
	Persistent

	value     string // persistent state
	_v_cookie string // volatile in-RAM only state; not managed by persistency layer
}

func NewMyObject(jar *Connection) *MyObject {
	return NewPersistent(reflect.TypeOf(MyObject{}), jar).(*MyObject)
}

type myObjectState MyObject

func (o *myObjectState) DropState() {
	o.value = ""
}

func (o *myObjectState) PySetState(pystate interface{}) error {
	s, ok := pystate.(string)
	if !ok {
		return fmt.Errorf("myobject: setstate: want str; got %T", pystate)
	}

	o.value = s
	return nil
}

func (o *myObjectState) PyGetState() interface{} {
	return o.value
}

// Persistent that is not registered to ZODB.
type Unregistered struct {
	Persistent
}

func init() {
	t := reflect.TypeOf
	RegisterClass("t.zodb.MyObject", t(MyObject{}), t(myObjectState{}))
	RegisterClassAlias("t.zodb.MyOldObject", "t.zodb.MyObject")
}

// checkObj verifies current state of persistent object.
//
// one can bind checkObj to t via tCheckObj.
func checkObj(t testing.TB, obj IPersistent, jar *Connection, oid Oid, serial Tid, state ObjectState, refcnt int32) {
	t.Helper()
	xbase := reflect.ValueOf(obj).Elem().FieldByName("Persistent")
	pbase := xbase.Addr().Interface().(*Persistent)

	var badv []string
	badf := func(format string, argv ...interface{}) {
		badv = append(badv, fmt.Sprintf(format, argv...))
	}

	zc := pbase.zclass
	//zc.class
	if typ := reflect.TypeOf(obj).Elem(); typ != zc.typ {
		badf("invalid zclass: .typ = %s  ; want %s", zc.typ, typ)
	}
	//zc.stateType

	if pbase.jar != jar {
		badf("invalid jar")
	}
	if pbase.oid != oid {
		badf("invalid oid: %s  ; want %s", pbase.oid, oid)
	}
	if pbase.serial != serial {
		badf("invalid serial: %s  ; want %s", pbase.serial, serial)
	}
	if pbase.state != state {
		badf("invalid state: %s  ; want %s", pbase.state, state)
	}
	if pbase.refcnt != refcnt {
		badf("invalid refcnt: %d  ; want %d", pbase.refcnt, refcnt)
	}
	if pbase.instance != obj {
		badf("base.instance != obj")
	}

	if len(badv) != 0 {
		msg := fmt.Sprintf("%#v:\n", obj)
		for _, bad := range badv {
			msg += fmt.Sprintf("\t- %s\n", bad)
		}
		t.Fatal(msg)
	}
}

// tCheckObj binds checkObj to t.
func tCheckObj(t testing.TB) func(IPersistent, *Connection, Oid, Tid, ObjectState, int32) {
	return func(obj IPersistent, jar *Connection, oid Oid, serial Tid, state ObjectState, refcnt int32) {
		t.Helper()
		checkObj(t, obj, jar, oid, serial, state, refcnt)
	}
}

// basic Persistent tests without storage.
func TestPersistentBasic(t *testing.T) {
	assert := assert.New(t)
	checkObj := tCheckObj(t)

	// unknown type -> Broken
	xobj := newGhost("t.unknown", 10, nil)
	b, ok := xobj.(*Broken)
	if !ok {
		t.Fatalf("unknown -> %T;  want Broken", xobj)
	}

	checkObj(b, nil, 10, InvalidTid, GHOST, 0)
	assert.Equal(b.class, "t.unknown")
	assert.Equal(b.state, (*mem.Buf)(nil))


	// t.zodb.MyObject -> *MyObject
	xobj = newGhost("t.zodb.MyObject", 11, nil)
	obj, ok := xobj.(*MyObject)
	if !ok {
		t.Fatalf("t.zodb.MyObject -> %T;  want MyObject", xobj)
	}

	checkObj(obj, nil, 11, InvalidTid, GHOST, 0)
	assert.Equal(ClassOf(obj), "t.zodb.MyObject")

	// t.zodb.MyOldObject -> *MyObject
	xobj = newGhost("t.zodb.MyOldObject", 12, nil)
	obj, ok = xobj.(*MyObject)
	if !ok {
		t.Fatalf("t.zodb.MyOldObject -> %T;  want MyObject", xobj)
	}

	checkObj(obj, nil, 12, InvalidTid, GHOST, 0)
	assert.Equal(ClassOf(obj), "t.zodb.MyObject")

	// ClassOf(unregistered-obj)
	obj2 := &Unregistered{}
	assert.Equal(ClassOf(obj2), `ZODB.Go("lab.nexedi.com/kirr/neo/go/zodb.Unregistered")`)

	// deactivate refcnt < 0  -> panic
	func() {
		defer func() {
			r := recover()
			if r == nil {
				t.Fatal("deactivate refcnt < 0: not panicked")
			}
			ehave := fmt.Sprintf("%s", r)
			ewant := fmt.Sprintf("t.zodb.MyObject(%s): deactivate: refcnt < 0  (= -1)", Oid(12))
			if ehave != ewant {
				t.Fatalf("deactivate refcnt < 0: panic error unexpected:\nhave: %q\nwant: %q", ehave, ewant)
			}
		}()

		obj.PDeactivate()
	}()
}

// ---- TestPersistentDB ----

// zcacheControl is simple live cache control that is organized as `{} oid ->
// PCachePolicy` table.
type zcacheControl struct {
	pCachePolicy map[Oid]PCachePolicy
}

func (cc *zcacheControl) PCacheClassify(obj IPersistent) PCachePolicy {
	return cc.pCachePolicy[obj.POid()] // default -> 0
}

// tDB represents testing database.
//
// The database can be worked on via zodb/go and committed into via zodb/py.
type tDB struct {
	*testing.T

	work string // working directory
	zurl string // zurl for database under work

	// zodb/go stor/db handle for the database
	stor IStorage
	db   *DB

	head    Tid           // last committed transaction
	commitq []IPersistent // queue to be committed

	// testdb options
	rawcache bool
}

// tConnection represents testing Connection.
type tConnection struct {
	*testing.T

	// a transaction and DB connection opened under it
	txn  transaction.Transaction
	ctx  context.Context
	conn *Connection
}

// testdb creates and initializes new test database.
func testdb(t0 *testing.T, rawcache bool) *tDB {
	t0.Helper()
	t := &tDB{
		T:        t0,
		rawcache: rawcache,
	}
	X := t.fatalif

	work, err := ioutil.TempDir("", "t-persistent"); X(err)
	t.work = work
	t.zurl = work + "/1.fs"

	finishok := false
	defer func() {
		if !finishok {
			err := os.RemoveAll(t.work); X(err)
		}
	}()

	// create test db via py with 2 objects
	t.Add(11, "init")
	t.Add(12, "db")
	t.Commit()

	// open the db via zodb/go
	t.Reopen()

	finishok = true
	return t
}

// Reopen reopens zodb/go .stor and .db .
func (t *tDB) Reopen() {
	t.Helper()
	X := t.fatalif

	t.close()
	stor, err := Open(context.Background(), t.zurl, &OpenOptions{
		ReadOnly: true,
		NoCache: !t.rawcache,
	}); X(err)
	db := NewDB(stor, &DBOptions{})
	t.stor = stor
	t.db = db
}

func (t *tDB) close() {
	t.Helper()
	X := t.fatalif

	if t.db != nil {
		err := t.db.Close(); X(err)
		t.db = nil
	}
	if t.stor != nil {
		err := t.stor.Close(); X(err)
		t.stor = nil
	}

}

// Close release resources associated with test database.
func (t *tDB) Close() {
	t.Helper()
	X := t.fatalif

	t.close()
	err := os.RemoveAll(t.work); X(err)
}

// Add marks object with oid as modified and queues it to be committed as
// MyObject(value).
//
// The commit is performed by Commit.
func (t *tDB) Add(oid Oid, value string) {
	obj := NewMyObject(nil) // XXX hack - goes without jar
	obj.oid = oid
	obj.value = value
	t.commitq = append(t.commitq, obj)
}

// Commit commits objects queued by Add.
func (t *tDB) Commit() Tid {
	t.Helper()

	head, err := ZPyCommit(t.zurl, t.head, t.commitq...)
	if err != nil {
		t.Fatal(err)
	}
	t.head = head
	t.commitq = nil
	return head
}

// CommitRaw commits raw changes.
func (t *tDB) CommitRaw(rawobjv ...ZRawObject) Tid {
	t.Helper()

	head, err := ZPyCommitRaw(t.zurl, t.head, rawobjv...)
	if err != nil {
		t.Fatal(err)
	}
	t.head = head
	return head
}

// Open opens new test transaction/connection.
func (t *tDB) Open(opt *ConnOptions) *tConnection {
	t.Helper()
	X := t.fatalif

	txn, ctx := transaction.New(context.Background())
	conn, err := t.db.Open(ctx, opt); X(err)

	assert.Same(t, conn.db, t.db)
	assert.Same(t, conn.txn, txn)

	return &tConnection{
		T:    t.T,
		txn:  txn,
		ctx:  ctx,
		conn: conn,
	}
}

// Get gets oid from t.conn and asserts its type.
func (t *tConnection) Get(oid Oid) *MyObject {
	t.Helper()
	xobj, err := t.conn.Get(t.ctx, oid)
	if err != nil {
		t.Fatal(err)
	}

	zclass := ClassOf(xobj)
	zmy    := "t.zodb.MyObject"
	if zclass != zmy {
		t.Fatalf("get %d: got %s;  want %s", oid, zclass, zmy)
	}

	return xobj.(*MyObject)
}

// PActivate activates obj in t environment.
func (t *tConnection) PActivate(obj IPersistent) {
	t.Helper()
	err := obj.PActivate(t.ctx)
	if err != nil {
		t.Fatal(err)
	}
}

// checkObj checks state of obj and that obj ∈ t.conn.
//
// if object is !GHOST - it also verifies its value.
func (t *tConnection) checkObj(obj *MyObject, oid Oid, serial Tid, state ObjectState, refcnt int32, valueOk ...string) {
	t.Helper()

	// any object with live pointer to it must be also in conn's cache.
	cache := t.conn.Cache()
	cache.Lock()
	connObj := cache.Get(oid)
	cache.Unlock()
	if obj != connObj {
		t.Fatalf("cache.get %s -> not same object:\nhave: %#v\nwant: %#v", oid, connObj, obj)
	}

	// and conn.Get must return exactly obj.
	connObj, err := t.conn.Get(t.ctx, oid)
	if err != nil {
		t.Fatal(err)
	}
	if obj != connObj {
		t.Fatalf("conn.get %s -> not same object:\nhave: %#v\nwant: %#v", oid, connObj, obj)
	}

	checkObj(t.T, obj, t.conn, oid, serial, state, refcnt)

	if state == GHOST {
		if len(valueOk) != 0 {
			panic("t.checkObj(GHOST) must come without value")
		}
		return
	}

	if len(valueOk) != 1 {
		panic("t.checkObj(!GHOST) must come with one value")
	}
	value := valueOk[0]
	if obj.value != value {
		t.Fatalf("obj.value mismatch: have %q;  want %q", obj.value, value)
	}
}

// Resync resyncs t to new transaction @at.
func (t *tConnection) Resync(at Tid) {
	t.Helper()
	db := t.conn.db

	txn, ctx := transaction.New(context.Background())
	err := t.conn.Resync(ctx, at)
	if err != nil {
		t.Fatalf("resync %s -> %s", at, err)
	}

	t.txn = txn
	t.ctx = ctx

	assert.Same(t, t.conn.db,  db)
	assert.Same(t, t.conn.txn, t.txn)
	assert.Equal(t, t.conn.At(), at)
}

// Abort aborts t's connection and verifies it becomes !live.
func (t *tConnection) Abort() {
	t.Helper()
	assert.Same(t, t.conn.txn, t.txn)
	t.txn.Abort()
	assert.Equal(t, t.conn.txn, nil)
}

func (t *tDB) fatalif(err error) {
	t.Helper()
	if err != nil {
		t.Fatal(err)
	}
}

func (t *tConnection) fatalif(err error) {
	t.Helper()
	if err != nil {
		t.Fatal(err)
	}
}

// Persistent tests with storage.
//
// this test covers everything at application-level: Persistent, DB, Connection
// and somewhat LiveCache.
func TestPersistentDB(t *testing.T) {
	// perform tests without and with raw data cache.
	// (rawcache=y verifies how raw cache handles invalidations)
	t.Run("rawcache=n", func(t *testing.T) { testPersistentDB(t, false) })
	t.Run("rawcache=y", func(t *testing.T) { testPersistentDB(t, true) })
}

func testPersistentDB(t0 *testing.T, rawcache bool) {
	assert := assert.New(t0)

	tdb := testdb(t0, rawcache)
	defer tdb.Close()

	tdb.Add(101, "bonjour")
	tdb.Add(102, "monde")
	at0 := tdb.Commit()

	tdb.Add(101, "hello")
	tdb.Add(102, "world")
	at1 := tdb.Commit()

	tdb.Reopen() // so that at0 is not covered by db.δtail
	db := tdb.db

	t1 := tdb.Open(&ConnOptions{})
	t := t1
	assert.Equal(t.conn.At(), at1)
	assert.Equal(db.pool, []*Connection(nil))

	// δtail coverage is (at1, at1]  (at0 not included)
	assert.Equal(db.δtail.Tail(), at1)
	assert.Equal(db.δtail.Head(), at1)

	// do not evict obj2 from live cache. obj1 is ok to be evicted.
	zcc := &zcacheControl{map[Oid]PCachePolicy{
		102: PCachePinObject | PCacheKeepState,
	}}

	zcache1 := t.conn.Cache()
	zcache1.Lock()
	zcache1.SetControl(zcc)
	zcache1.Unlock()

	// get objects
	obj1 := t.Get(101)
	obj2 := t.Get(102)
	t.checkObj(obj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(obj2, 102, InvalidTid, GHOST, 0)

	// activate:		jar has to load, state changes -> uptodate
	t.PActivate(obj1)
	t.PActivate(obj2)
	t.checkObj(obj1, 101, at1, UPTODATE, 1, "hello")
	t.checkObj(obj2, 102, at1, UPTODATE, 1, "world")

	// activate again:	refcnt++
	t.PActivate(obj1)
	t.PActivate(obj2)
	t.checkObj(obj1, 101, at1, UPTODATE, 2, "hello")
	t.checkObj(obj2, 102, at1, UPTODATE, 2, "world")

	// deactivate:		refcnt--
	obj1.PDeactivate()
	obj2.PDeactivate()
	t.checkObj(obj1, 101, at1, UPTODATE, 1, "hello")
	t.checkObj(obj2, 102, at1, UPTODATE, 1, "world")

	// deactivate:		state dropped for obj1, obj2 stays in live cache
	obj1.PDeactivate()
	obj2.PDeactivate()
	t.checkObj(obj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(obj2, 102, at1, UPTODATE,    0, "world")

	// invalidate:		obj2 state dropped
	obj1.PInvalidate()
	obj2.PInvalidate()
	t.checkObj(obj1, 101, InvalidTid, GHOST,    0)
	t.checkObj(obj2, 102, InvalidTid, GHOST,    0)

	// commit change to obj2 from external process
	tdb.Add(102, "kitty")
	at2 := tdb.Commit()

	// new db connection should see the change
	t2 := tdb.Open(&ConnOptions{})
	assert.Equal(t2.conn.At(), at2)
	assert.Equal(db.pool, []*Connection(nil))

	// δtail coverage is (at1, at2]
	assert.Equal(db.δtail.Tail(), at1)
	assert.Equal(db.δtail.Head(), at2)

	c2obj1 := t2.Get(101)
	c2obj2 := t2.Get(102)
	t2.checkObj(c2obj1, 101, InvalidTid, GHOST, 0)
	t2.checkObj(c2obj2, 102, InvalidTid, GHOST, 0)

	t2.PActivate(c2obj1)
	t2.PActivate(c2obj2)
	t2.checkObj(c2obj1, 101, at1, UPTODATE, 1, "hello")
	t2.checkObj(c2obj2, 102, at2, UPTODATE, 1, "kitty")
	c2obj1.PDeactivate()
	c2obj2.PDeactivate()


	// conn1 stays at older view for now
	t1.checkObj(obj1, 101, InvalidTid, GHOST,    0)
	t1.checkObj(obj2, 102, InvalidTid, GHOST,    0)
	t1.PActivate(obj1)
	t1.PActivate(obj2)
	t1.checkObj(obj1, 101, at1, UPTODATE, 1, "hello")
	t1.checkObj(obj2, 102, at1, UPTODATE, 1, "world")

	// conn1 deactivate:	obj2 stays in conn1 live cache with old state
	obj1.PDeactivate()
	obj2.PDeactivate()
	t1.checkObj(obj1, 101, InvalidTid, GHOST, 0)
	t1.checkObj(obj2, 102, at1, UPTODATE,    0, "world")

	// txn1 completes - conn1 goes back to db pool
	t1.Abort()
	assert.Equal(db.pool, []*Connection{t1.conn})


	// open new connection - it should be conn1 but at updated database view
	t3 := tdb.Open(&ConnOptions{})
	assert.Same(t3.conn, t1.conn)
	t = t3
	assert.Equal(t.conn.At(), at2)
	assert.Equal(db.pool, []*Connection{})

	// obj2 should be invalidated
	t.checkObj(obj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(obj2, 102, InvalidTid, GHOST, 0)

	// obj2 data should be new
	t.PActivate(obj1)
	t.PActivate(obj2)
	t.checkObj(obj1, 101, at1, UPTODATE, 1, "hello")
	t.checkObj(obj2, 102, at2, UPTODATE, 1, "kitty")

	obj1.PDeactivate()
	obj2.PDeactivate()
	t.checkObj(obj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(obj2, 102, at2, UPTODATE,    0, "kitty")


	// newly created object should not go to ghost on deactivate
	obj3 := NewMyObject(t.conn)
	obj3.value = "new"
	checkObj(t0, obj3, t.conn, InvalidOid, InvalidTid, UPTODATE, 0)
	assert.Equal(obj3.value, "new")
	t.PActivate(obj3)
	checkObj(t0, obj3, t.conn, InvalidOid, InvalidTid, UPTODATE, 1)
	obj3.PDeactivate()
	checkObj(t0, obj3, t.conn, InvalidOid, InvalidTid, UPTODATE, 0)
	assert.Equal(obj3.value, "new")


	// finish tnx3 and txn2 - conn1 and conn2 go back to db pool
	t.Abort()
	t2.Abort()
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})


	// ---- Resync ----

	// open new connection in nopool mode to verify resync
	t4 := tdb.Open(&ConnOptions{NoPool: true})
	t = t4
	assert.Equal(t.conn.At(), at2)
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})

	// pin obj2 into live cache, similarly to conn1
	rzcache := t.conn.Cache()
	rzcache.Lock()
	rzcache.SetControl(zcc)
	rzcache.Unlock()

	// it should see latest data
	robj1 := t.Get(101)
	robj2 := t.Get(102)
	t.checkObj(robj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(robj2, 102, InvalidTid, GHOST, 0)

	t.PActivate(robj1)
	t.PActivate(robj2)
	t.checkObj(robj1, 101, at1, UPTODATE, 1, "hello")
	t.checkObj(robj2, 102, at2, UPTODATE, 1, "kitty")

	// obj2 stays in live cache
	robj1.PDeactivate()
	robj2.PDeactivate()
	t.checkObj(robj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(robj2, 102, at2, UPTODATE,    0, "kitty")

	// txn4 completes, but its conn stays out of db pool
	t.Abort()
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})

	// Resync ↓ (at2 -> at1; within δtail coverage)
	t.Resync(at1)
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})

	// obj2 should be invalidated
	t.checkObj(robj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(robj2, 102, InvalidTid, GHOST, 0)

	// obj2 data should be old
	t.PActivate(robj1)
	t.PActivate(robj2)
	t.checkObj(robj1, 101, at1, UPTODATE, 1, "hello")
	t.checkObj(robj2, 102, at1, UPTODATE, 1, "world")

	robj1.PDeactivate()
	robj2.PDeactivate()
	t.checkObj(robj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(robj2, 102, at1, UPTODATE, 0, "world")

	// Resync ↑ (at1 -> at2; within δtail coverage)
	t.Abort()
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})
	t.Resync(at2)
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})

	// obj2 should be invalidated
	t.checkObj(robj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(robj2, 102, InvalidTid, GHOST, 0)

	t.PActivate(robj1)
	t.PActivate(robj2)
	t.checkObj(robj1, 101, at1, UPTODATE, 1, "hello")
	t.checkObj(robj2, 102, at2, UPTODATE, 1, "kitty")

	robj1.PDeactivate()
	robj2.PDeactivate()
	t.checkObj(robj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(robj2, 102, at2, UPTODATE, 0, "kitty")

	// Resync ↓ (at2 -> at0; to outside δtail coverage)
	t.Abort()
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})
	t.Resync(at0)
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})

	// obj2 should be invalidated
	t.checkObj(robj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(robj2, 102, InvalidTid, GHOST, 0)

	t.PActivate(robj1)
	t.PActivate(robj2)
	t.checkObj(robj1, 101, at0, UPTODATE, 1, "bonjour")
	t.checkObj(robj2, 102, at0, UPTODATE, 1, "monde")

	robj1.PDeactivate()
	robj2.PDeactivate()
	t.checkObj(robj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(robj2, 102, at0, UPTODATE, 0, "monde")

	// Resync ↑ (at0 -> at2; from outside δtail coverage)
	t.Abort()
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})
	t.Resync(at2)
	assert.Equal(db.pool, []*Connection{t1.conn, t2.conn})

	// obj2 should be invalidated
	t.checkObj(robj1, 101, InvalidTid, GHOST, 0)
	t.checkObj(robj2, 102, InvalidTid, GHOST, 0)
}

// Verify that PActivate works correctly after hitting an error from the storage.
// In this test the error is "object was deleted".
func TestActivateAfterDelete(t0 *testing.T) {
	assert := assert.New(t0)

	tdb := testdb(t0, /*rawcache=*/false)
	defer tdb.Close()
	db := tdb.db

	tdb.Add(101, "object")
	at0 := tdb.Commit()

	t := tdb.Open(&ConnOptions{})

	// do not evict the object from live cache.
	zcc := &zcacheControl{map[Oid]PCachePolicy{
		101: PCachePinObject | PCacheKeepState,
	}}
	zcache := t.conn.Cache()
	zcache.Lock()
	zcache.SetControl(zcc)
	zcache.Unlock()

	// load the object
	obj := t.Get(101)
	t.checkObj(obj, 101, InvalidTid, GHOST, 0)
	t.PActivate(obj)
	t.checkObj(obj, 101, at0, UPTODATE, 1, "object")
	obj.PDeactivate()
	t.checkObj(obj, 101, at0, UPTODATE, 0, "object")

	// delete obj
	at1 := tdb.CommitRaw(ZRawObject{Oid: 101, Data: nil})

	// conn stays at older view with obj pinned into the cache
	t.checkObj(obj, 101, at0, UPTODATE, 0, "object")

	// finish transaction and reopen new connection - it should be the same conn
	t.Abort()
	assert.Equal(db.pool, []*Connection{t.conn})
	t_ := tdb.Open(&ConnOptions{})
	assert.Same(t_.conn, t.conn)
	t = t_
	assert.Equal(t.conn.At(), at1)

	// obj should be invalidated but present in the cache
	t.checkObj(obj, 101, InvalidTid, GHOST, 0)

	// activating obj should give "no data" error
	// loop because second activate used to panic
	for i := 0; i < 10; i++ {
		err := obj.PActivate(t.ctx)
		eok := &NoDataError{Oid: obj.POid(), DeletedAt: at1}
		var e *NoDataError
		errors.As(err, &e)
		if !reflect.DeepEqual(e, eok) {
			t.Fatalf("(%d) after delete: err:\nhave: %s\nwant cause: %s", i, err, eok)
		}
		// obj should stay in the cache in ghost state
		t.checkObj(obj, 101, InvalidTid, GHOST, 0)
	}
}

// Test details of how LiveCache handles live caching policy.
func TestLiveCache(t0 *testing.T) {
	assert := assert.New(t0)

	tdb := testdb(t0, /*rawcache=*/false)
	defer tdb.Close()

	tdb.Add(101, "мир")
	tdb.Add(102, "труд")
	tdb.Add(103, "май")
	tdb.Add(104, "весна")
	at1 := tdb.Commit()

	zcc := &zcacheControl{map[Oid]PCachePolicy{
		// obj1 - default (currently: don't pin and don't keep state)
		// TODO: it should go into LRU on release, not dropped
		101: 0,
		// objPK - pin and keep state
		102: PCachePinObject | PCacheKeepState,
		// objPD - pin, but don't keep state
		103: PCachePinObject | PCacheDropState,
		// objDD - drop object and state
		104: PCacheDropObject | PCacheDropState,
		// XXX objDK ?
	}}

	t := tdb.Open(&ConnOptions{})
	zcache := t.conn.Cache()
	zcache.Lock()
	zcache.SetControl(zcc)
	zcache.Unlock()

	// get objects
	obj := t.Get(101)
	objPK := t.Get(102)
	objPD := t.Get(103)
	objDD := t.Get(104)
	t.checkObj(obj,   101, InvalidTid, GHOST, 0)
	t.checkObj(objPK, 102, InvalidTid, GHOST, 0)
	t.checkObj(objPD, 103, InvalidTid, GHOST, 0)
	t.checkObj(objDD, 104, InvalidTid, GHOST, 0)

	// activate	* -> uptodate
	t.PActivate(obj)
	t.PActivate(objPK)
	t.PActivate(objPD)
	t.PActivate(objDD)
	t.checkObj(obj,   101, at1, UPTODATE, 1, "мир")
	t.checkObj(objPK, 102, at1, UPTODATE, 1, "труд")
	t.checkObj(objPD, 103, at1, UPTODATE, 1, "май")
	t.checkObj(objDD, 104, at1, UPTODATE, 1, "весна")

	// deactivate	obj{,PD,DD} drop state, objPK is kept
	obj.PDeactivate()
	objPK.PDeactivate()
	objPD.PDeactivate()
	objDD.PDeactivate()
	t.checkObj(obj,   101, InvalidTid, GHOST, 0)
	t.checkObj(objPK, 102, at1, UPTODATE, 0, "труд")
	t.checkObj(objPD, 103, InvalidTid, GHOST, 0)
	t.checkObj(objDD, 104, InvalidTid, GHOST, 0)

	// live cache should keep pinned object present even if we drop all
	// regular pointers to it and do GC.
	obj._v_cookie = "peace"
	objPK._v_cookie = "labour"
	objPD._v_cookie = "may"
	objDD._v_cookie = "spring"
	obj = nil
	objPK = nil
	objPD = nil
	objDD = nil
	for i := 0; i < 10; i++ {
		runtime.GC() // need only 2 runs since cache uses finalizers
	}

	xobj := zcache.Get(101)
	xobjPK := zcache.Get(102)
	xobjPD := zcache.Get(103)
	xobjDD := zcache.Get(104)
	assert.Equal(xobj, nil)
	assert.NotEqual(xobjPK, nil)
	assert.NotEqual(xobjPD, nil)
	assert.Equal(xobjDD, nil)
	objPK = xobjPK.(*MyObject)
	objPD = xobjPD.(*MyObject)
	t.checkObj(objPK, 102, at1, UPTODATE,      0, "труд")
	t.checkObj(objPD, 103, InvalidTid, GHOST,  0)

	assert.Equal(objPK._v_cookie, "labour")
	assert.Equal(objPD._v_cookie, "may")

	obj = t.Get(101)
	objDD = t.Get(104)
	t.checkObj(obj,   101, InvalidTid, GHOST,  0)
	t.checkObj(objDD, 104, InvalidTid, GHOST,  0)

	assert.Equal(obj._v_cookie, "")
	assert.Equal(objDD._v_cookie, "")

	// TODO reclassify tests
}

// TODO Map & List tests.


// TODO PyGetState vs PySetState tests (general - for any type):
//
// db1: produced by zodb/py
// go: load db1
// go: commit -> db2 (resave)
// go: load db2
// go: check (loaded from db2) == (loaded from db1)
