// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT

package generic

import (
	"context"
	"testing"

	"code.forgejo.org/f3/gof3/v3/f3"
	"code.forgejo.org/f3/gof3/v3/id"
	"code.forgejo.org/f3/gof3/v3/kind"
	"code.forgejo.org/f3/gof3/v3/logger"
	"code.forgejo.org/f3/gof3/v3/path"

	"github.com/stretchr/testify/assert"
)

type compareFormat struct {
	f3.Common
	V int
}

func (o *compareFormat) Clone() f3.Interface {
	clone := &compareFormat{}
	*clone = *o
	return clone
}

type compareNodeDriver struct {
	NullDriver
	v  int
	ID string
}

func (o *compareNodeDriver) NewFormat() f3.Interface {
	return &compareFormat{
		Common: f3.NewCommon(o.ID),
	}
}

func (o *compareNodeDriver) ToFormat() f3.Interface {
	f := o.NewFormat().(*compareFormat)
	f.V = o.v
	return f
}

func (o *compareNodeDriver) FromFormat(f f3.Interface) {
	fc := f.(*compareFormat)
	o.v = fc.V
	o.ID = fc.GetID()
}

func newCompareNodeDriver() NodeDriverInterface {
	return &compareNodeDriver{}
}

type compareTreeDriver struct {
	NullTreeDriver
}

func newCompareTreeDriver() TreeDriverInterface {
	return &compareTreeDriver{}
}

func (o *compareTreeDriver) Factory(ctx context.Context, kind kind.Kind) NodeDriverInterface {
	d := newCompareNodeDriver()
	d.SetTreeDriver(o)
	return d
}

func newCompareTree() TreeInterface {
	tree := &testTree{}
	tree.Init(tree, newTestOptions())
	tree.Trace("init done")
	tree.SetDriver(newCompareTreeDriver())
	tree.Register(kindCompareNode, func(ctx context.Context, kind kind.Kind) NodeInterface {
		node := &compareNode{}
		return node.Init(node)
	})
	return tree
}

var kindCompareNode = kind.Kind("compare")

type compareNode struct {
	Node
}

func TestTreeCompare(t *testing.T) {
	tree := newTestTree()
	log := logger.NewCaptureLogger()
	log.SetLevel(logger.Trace)
	tree.SetLogger(log)

	root := tree.Factory(context.Background(), kindTestNodeLevelOne)
	tree.SetRoot(root)
	assert.True(t, TreeCompare(context.Background(), tree, path.NewPathFromString(NewElementNode, ""), tree, path.NewPathFromString(NewElementNode, "")))

	log.Reset()
	assert.False(t, TreeCompare(context.Background(), tree, path.NewPathFromString(NewElementNode, "/notfound"), tree, path.NewPathFromString(NewElementNode, "")))
	assert.Contains(t, log.String(), "a does not have /notfound")

	log.Reset()
	assert.False(t, TreeCompare(context.Background(), tree, path.NewPathFromString(NewElementNode, "/"), tree, path.NewPathFromString(NewElementNode, "/notfound")))
	assert.Contains(t, log.String(), "b does not have /notfound")
}

func TestNodeCompare(t *testing.T) {
	log := logger.NewCaptureLogger()
	log.SetLevel(logger.Trace)

	treeA := newCompareTree()
	treeA.SetLogger(log)

	treeB := newCompareTree()
	treeB.SetLogger(log)

	nodeA := treeA.Factory(context.Background(), kindCompareNode)
	nodeA.SetID(id.NewNodeID("root"))
	assert.True(t, NodeCompare(context.Background(), nodeA, nodeA))

	t.Run("different kind", func(t *testing.T) {
		log.Reset()
		other := treeB.Factory(context.Background(), kindCompareNode)
		other.SetKind("other")
		assert.False(t, NodeCompare(context.Background(), nodeA, other))
		assert.Contains(t, log.String(), "kind is different")
	})

	t.Run("difference", func(t *testing.T) {
		log.Reset()
		other := treeB.Factory(context.Background(), kindCompareNode)
		other.FromFormat(&compareFormat{V: 123456})

		assert.False(t, NodeCompare(context.Background(), nodeA, other))
		assert.Contains(t, log.String(), "difference")
		assert.Contains(t, log.String(), "123456")
	})

	t.Run("children count", func(t *testing.T) {
		log.Reset()
		other := treeB.Factory(context.Background(), kindCompareNode)
		other.SetChild(treeB.Factory(context.Background(), kindCompareNode))

		assert.False(t, NodeCompare(context.Background(), nodeA, other))
		assert.Contains(t, log.String(), "children count")
	})

	nodeAA := treeA.Factory(context.Background(), kindCompareNode)
	nodeAA.SetID(id.NewNodeID("levelone"))
	nodeA.SetChild(nodeAA)

	nodeB := treeB.Factory(context.Background(), kindCompareNode)
	nodeB.SetID(id.NewNodeID("root"))

	t.Run("children are the same", func(t *testing.T) {
		nodeBB := treeB.Factory(context.Background(), kindCompareNode)
		nodeBB.SetID(id.NewNodeID("levelone"))
		nodeB.SetChild(nodeBB)

		assert.True(t, NodeCompare(context.Background(), nodeA, nodeB))

		nodeB.DeleteChild(nodeBB.GetID())
	})

	t.Run("children have different IDs", func(t *testing.T) {
		log.Reset()

		nodeBB := treeB.Factory(context.Background(), kindCompareNode)
		nodeBB.SetID(id.NewNodeID("SOMETHINGELSE"))
		nodeB.SetChild(nodeBB)

		assert.False(t, NodeCompare(context.Background(), nodeA, nodeB))
		assert.Contains(t, log.String(), "id levelone matching the child")

		nodeB.DeleteChild(nodeBB.GetID())
	})

	t.Run("children have different content", func(t *testing.T) {
		log.Reset()

		nodeBB := treeB.Factory(context.Background(), kindCompareNode)
		nodeBB.FromFormat(&compareFormat{V: 12345678})
		nodeBB.SetID(id.NewNodeID("levelone"))
		nodeB.SetChild(nodeBB)

		assert.False(t, NodeCompare(context.Background(), nodeA, nodeB))
		assert.Contains(t, log.String(), "difference")
		assert.Contains(t, log.String(), "12345678")

		nodeB.DeleteChild(nodeBB.GetID())
	})

	t.Run("children are the same because of their mapped ID", func(t *testing.T) {
		log.Reset()

		nodeBB := treeB.Factory(context.Background(), kindCompareNode)
		nodeBB.SetID(id.NewNodeID("REMAPPEDID"))
		nodeB.SetChild(nodeBB)

		nodeAA.SetMappedID(id.NewNodeID("REMAPPEDID"))

		assert.True(t, NodeCompare(context.Background(), nodeA, nodeB))

		nodeB.DeleteChild(nodeBB.GetID())
		nodeAA.SetMappedID(id.NilID)
	})

	t.Run("children are different because of their mapped ID", func(t *testing.T) {
		log.Reset()

		nodeBB := treeB.Factory(context.Background(), kindCompareNode)
		nodeBB.SetID(id.NewNodeID("levelone"))
		nodeB.SetChild(nodeBB)

		nodeAA.SetMappedID(id.NewNodeID("REMAPPEDID"))

		assert.False(t, NodeCompare(context.Background(), nodeA, nodeB))
		assert.Contains(t, log.String(), "id REMAPPEDID matching the child")

		nodeB.DeleteChild(nodeBB.GetID())
		nodeAA.SetMappedID(id.NilID)
	})
}
