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

package f3

import (
	"context"
	"slices"
	"testing"
	"time"

	"code.forgejo.org/f3/gof3/v3/f3"
	filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
	tests_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/tests/repository"
	"code.forgejo.org/f3/gof3/v3/id"
	"code.forgejo.org/f3/gof3/v3/kind"
	"code.forgejo.org/f3/gof3/v3/path"
	f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
	"code.forgejo.org/f3/gof3/v3/tree/generic"
	tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"

	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type remapper func(path string) path.Path

func ForgeCompliance(t *testing.T, name string) {
	testForge := tests_forge.GetFactory(name)()

	ctx := context.Background()

	forgeOptions := testForge.NewOptions(t)
	forgeTree := generic.GetFactory("f3")(ctx, forgeOptions)

	forgeTree.Trace("======= build fixture")
	fixtureTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
	TreeBuildPartial(t, name, testForge.GetKindExceptions(), forgeOptions, fixtureTree)

	forgeTree.Trace("======= mirror fixture to forge")

	generic.TreeMirror(ctx, fixtureTree, forgeTree, generic.NewPathFromString(""), generic.NewMirrorOptions())

	forgeTree.Trace("======= run compliance tests")

	remap := func(p string) path.Path {
		return generic.TreePathRemap(ctx, fixtureTree, generic.NewPathFromString(p), forgeTree)
	}

	kindToForgePath := make(map[kind.Kind]string, len(KindToFixturePath))
	for kind, path := range KindToFixturePath {
		remappedPath := remap(path)
		if remappedPath.Empty() {
			forgeTree.Trace("%s was not mirrored, ignored", path)
			continue
		}
		kindToForgePath[kind] = remappedPath.String()
	}

	ComplianceKindTests(t, name, forgeTree.(f3_tree.TreeInterface), kindToForgePath, testForge.GetKindExceptions())
	ComplianceNameTests(t, name, forgeTree.(f3_tree.TreeInterface), remap, kindToForgePath, testForge.GetNameExceptions())

	if testForge.DeleteAfterCompliance() {
		TreeDelete(t, testForge.GetNonTestUsers(), forgeOptions, forgeTree)
	}
}

func ComplianceNameTests(t *testing.T, name string, tree f3_tree.TreeInterface, remap remapper, kindToForgePath map[kind.Kind]string, exceptions []string) {
	t.Helper()
	ctx := context.Background()
	t.Run("PullRequests", func(t *testing.T) {
		for _, variant := range []struct {
			name    string
			builder func(t *testing.T, tree f3_tree.TreeInterface, remap remapper, pullRequest *f3.PullRequest, parent path.Path) *f3.PullRequest
		}{
			{
				name:    tests_forge.ComplianceNameForkedPullRequest,
				builder: ComplianceForkedPullRequest,
			},
			{
				name:    tests_forge.ComplianceNameNoBranchPullRequest,
				builder: ComplianceNoBranchPullRequest,
			},
		} {
			if !slices.Contains(exceptions, variant.name) {
				t.Run(variant.name, func(t *testing.T) {
					kind := kind.Kind(f3_tree.KindPullRequests)
					p := kindToForgePath[kind]
					parent := tree.Find(generic.NewPathFromString(p))
					require.NotEqualValues(t, generic.NilNode, parent, p)
					child := tree.Factory(ctx, tree.GetChildrenKind(kind))
					childFormat := variant.builder(t, tree, remap, child.NewFormat().(*f3.PullRequest), parent.GetCurrentPath())

					child.FromFormat(childFormat)
					tree.Trace("'Upsert' the new pull request in the parent and store it in the forge")
					child.SetParent(parent)
					child.Upsert(ctx)
				})
			}
		}
	})
}

func ComplianceForkedPullRequest(t *testing.T, tree f3_tree.TreeInterface, remap remapper, pullRequest *f3.PullRequest, parent path.Path) *f3.PullRequest {
	user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
	projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
	repositoryNode := projectNode.Find(generic.NewPathFromString("repositories/vcs"))
	repositoryNode.Get(context.Background())
	repositoryHelper := tests_repository.NewTestHelper(t, "", repositoryNode)

	mainRef := "master"
	mainSha := repositoryHelper.GetRepositorySha(mainRef)

	tree.Trace("create a feature branch in the /forge/users/20222/projects/99099 fork")
	forkedRepositoryPath := remap("/forge/users/20222/projects/99099/repositories/vcs")
	forkedRepositoryNode := tree.Find(forkedRepositoryPath)
	require.NotEqual(t, forkedRepositoryNode, generic.NilNode)
	forkedRepositoryHelper := tests_repository.NewTestHelper(t, "", forkedRepositoryNode)
	featureRef := "generatedforkfeature"
	forkedRepositoryHelper.InternalBranchRepositoryFeature(featureRef, featureRef+" content")
	featureSha := forkedRepositoryHelper.GetRepositorySha(featureRef)
	forkedRepositoryHelper.PushMirror()

	now := now()
	prCreated := tick(&now)
	prUpdated := tick(&now)

	pullRequest.PosterID = f3_tree.NewUserReference(user.GetID())
	pullRequest.Title = featureRef + " pr title"
	pullRequest.Content = featureRef + " pr content"
	pullRequest.State = f3.PullRequestStateOpen
	pullRequest.IsLocked = false
	pullRequest.Created = prCreated
	pullRequest.Updated = prUpdated
	pullRequest.Closed = nil
	pullRequest.Merged = false
	pullRequest.MergedTime = nil
	pullRequest.MergeCommitSHA = ""
	pullRequest.Head = f3.PullRequestBranch{
		Ref:        featureRef,
		SHA:        featureSha,
		Repository: f3.NewReference(forkedRepositoryPath.String()),
	}
	pullRequest.Base = f3.PullRequestBranch{
		Ref:        mainRef,
		SHA:        mainSha,
		Repository: f3.NewReference("../../repository/vcs"),
	}
	return pullRequest
}

func ComplianceNoBranchPullRequest(t *testing.T, tree f3_tree.TreeInterface, remap remapper, pullRequest *f3.PullRequest, parent path.Path) *f3.PullRequest {
	user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
	projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
	repositoryNode := projectNode.Find(generic.NewPathFromString("repositories/vcs"))
	repositoryNode.Get(context.Background())
	repositoryHelper := tests_repository.NewTestHelper(t, "", repositoryNode)

	mainRef := "master"
	mainSha := repositoryHelper.GetRepositorySha(mainRef)

	tree.Trace("create a pull request from the SHA when the branch does not exist, for instance for closed pull requests for which the branch was deleted")
	featureRef := "nobranchfeature"
	repositoryHelper.InternalBranchRepositoryFeature(featureRef, featureRef+" content")
	featureSha := repositoryHelper.GetRepositorySha(featureRef)
	repositoryHelper.PushMirror()

	now := now()
	prCreated := tick(&now)
	prUpdated := tick(&now)

	pullRequest.PosterID = f3_tree.NewUserReference(user.GetID())
	pullRequest.Title = featureRef + " pr title"
	pullRequest.Content = featureRef + " pr content"
	pullRequest.State = f3.PullRequestStateOpen
	pullRequest.IsLocked = false
	pullRequest.Created = prCreated
	pullRequest.Updated = prUpdated
	pullRequest.Closed = nil
	pullRequest.Merged = false
	pullRequest.MergedTime = nil
	pullRequest.MergeCommitSHA = ""
	pullRequest.Head = f3.PullRequestBranch{
		Ref:        "nonexistentbranch",
		SHA:        featureSha,
		Repository: f3.NewReference("../../repository/vcs"),
	}
	pullRequest.Base = f3.PullRequestBranch{
		Ref:        mainRef,
		SHA:        mainSha,
		Repository: f3.NewReference("../../repository/vcs"),
	}
	return pullRequest
}

func ComplianceKindTests(t *testing.T, name string, tree f3_tree.TreeInterface, kindToForgePath map[kind.Kind]string, exceptions []kind.Kind) {
	t.Helper()
	exceptions = append(exceptions, f3_tree.KindRepositories)

	for _, kind := range KindWithFixturePath {
		path := kindToForgePath[kind]
		if !tree.IsContainer(kind) {
			continue
		}
		if slices.Contains(exceptions, kind) {
			continue
		}
		t.Run(string(kind), func(t *testing.T) {
			Compliance(t, name, tree, path, kind, GeneratorSetRandom, GeneratorModify)
		})
	}
}

func Compliance(t *testing.T, name string, tree f3_tree.TreeInterface, p string, kind kind.Kind, generator GeneratorFunc, modificator ModificatorFunc) {
	t.Helper()
	ctx := context.Background()

	tree.Trace("%s", p)
	parent := tree.Find(generic.NewPathFromString(p))
	require.NotEqualValues(t, generic.NilNode, parent, p)
	tree.Trace("create a new child in memory")
	child := tree.Factory(ctx, tree.GetChildrenKind(kind))
	childFormat := generator(t, name, child.NewFormat(), parent.GetCurrentPath())
	child.FromFormat(childFormat)
	if i := child.GetID(); i != id.NilID {
		tree.Trace("about to insert child %s", i)
		assert.EqualValues(t, generic.NilNode, parent.GetChild(child.GetID()))
	} else {
		tree.Trace("about to insert child with nil ID")
	}
	if child.GetDriver().IsNull() {
		t.Skip("no driver, skipping")
	}

	tree.Trace("'Upsert' the new child in the parent and store it in the forge")
	child.SetParent(parent)
	child.Upsert(ctx)
	tree.Trace("done inserting child '%s'", child.GetID())
	before := child.ToFormat()
	require.EqualValues(t, before.GetID(), child.GetID().String())
	tree.Trace("'Get' the child '%s' from the forge", child.GetID())
	child.Get(ctx)
	after := child.ToFormat()
	tree.Trace("check the F3 representations Upsert & Get to/from the forge are equivalent")
	require.True(t, cmp.Equal(before, after), cmp.Diff(before, after))

	tree.Trace("check the F3 representation FromFormat/ToFormat are identical")
	{
		saved := after

		a := childFormat.Clone()
		if tree.AllocateID() {
			a.SetID("123456")
		}
		child.FromFormat(a)
		b := child.ToFormat()
		require.True(t, cmp.Equal(a, b), cmp.Diff(a, b))

		child.FromFormat(saved)
	}

	if childFormat.GetName() != childFormat.GetID() {
		tree.Trace("'GetIDFromName' %s %s", kind, childFormat.GetName())
		id := parent.GetIDFromName(ctx, childFormat.GetName())
		assert.EqualValues(t, child.GetID(), id)
	}

	for i, modified := range modificator(t, after, parent.GetCurrentPath()) {
		tree.Trace("%d: %s 'Upsert' a modified child %v", i, kind, modified)
		child.FromFormat(modified)
		child.Upsert(ctx)
		tree.Trace("%d: 'Get' the modified child '%s' from the forge", i, child.GetID())
		child.Get(ctx)
		after = child.ToFormat()
		tree.Trace("%d: check the F3 representations Upsert & Get to/from the forge of the modified child are equivalent", i)
		require.True(t, cmp.Equal(modified, after), cmp.Diff(modified, after))
	}

	nodeChildren := parent.GetNodeChildren()
	tree.Trace("'ListPage' and only 'Get' known %d children of %s", len(nodeChildren), parent.GetKind())
	if len(nodeChildren) > 0 {
		parent.List(ctx)
		for _, child := range parent.GetChildren() {
			if _, ok := nodeChildren[child.GetID()]; ok {
				tree.Trace("'WalkAndGet' %s child %s %s", parent.GetCurrentPath().ReadableString(), child.GetKind(), child.GetID())
				child.WalkAndGet(ctx, parent.GetCurrentPath(), generic.NewWalkOptions(nil))
			}
		}
	}

	tree.Trace("'Delete' child '%s' from the forge", child.GetID())
	child.Delete(ctx)
	assert.EqualValues(t, generic.NilNode, parent.GetChild(child.GetID()))

	assert.True(t, child.GetIsSync())
	loop := 100
	for i := 0; i < loop; i++ {
		child.SetIsSync(false)
		child.Get(ctx)
		if !child.GetIsSync() {
			break
		}
		tree.Trace("waiting for asynchronous child deletion (%d/%d)", i, loop)
		time.Sleep(5 * time.Second)
	}
	assert.False(t, child.GetIsSync())

	tree.Trace("%s did something %s", kind, child)
}
