package server

import (
	"context"
	"testing"

	openfgav1 "github.com/openfga/api/proto/openfga/v1"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/stretchr/testify/require"

	authnlib "github.com/grafana/authlib/authn"
	claims "github.com/grafana/authlib/types"

	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
	"github.com/grafana/grafana/pkg/services/authz/zanzana/store"
	"github.com/grafana/grafana/pkg/services/sqlstore"
	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/tests/testsuite"
	"github.com/grafana/grafana/pkg/util/testutil"
)

const (
	namespace = "default"

	dashboardGroup    = "dashboard.grafana.app"
	dashboardResource = "dashboards"

	folderGroup    = "folder.grafana.app"
	folderResource = "folders"

	teamGroup    = "iam.grafana.app"
	teamResource = "teams"

	userGroup    = "iam.grafana.app"
	userResource = "users"

	serviceAccountGroup    = "iam.grafana.app"
	serviceAccountResource = "serviceaccounts"

	statusSubresource = "status"
)

func setup(t *testing.T, srv *Server) *Server {
	// seed tuples
	tuples := []*openfgav1.TupleKey{
		common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"),
		common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"),
		common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource, ""),
		common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource, ""),
		common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "", "1"),
		common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "1"),
		common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "3"),
		common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "1"),
		common.NewFolderTuple("user:6", common.RelationGet, "1"),
		common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource, ""),
		// folder-4 -> folder-5 -> folder-6
		common.NewFolderParentTuple("5", "4"),
		common.NewFolderParentTuple("6", "5"),
		common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "5"),
		common.NewFolderResourceTuple("user:9", common.RelationCreate, dashboardGroup, dashboardResource, "", "5"),
		common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "10"),
		common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "11"),
		common.NewGroupResourceTuple("user:11", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource),
		common.NewFolderResourceTuple("user:12", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "5"),
		common.NewFolderResourceTuple("user:13", common.RelationGet, folderGroup, folderResource, statusSubresource, "5"),
		common.NewTypedResourceTuple("user:14", common.RelationGet, common.TypeTeam, teamGroup, teamResource, statusSubresource, "1"),
		common.NewTypedResourceTuple("user:15", common.RelationGet, common.TypeUser, userGroup, userResource, statusSubresource, "1"),
		common.NewTypedResourceTuple("user:16", common.RelationGet, common.TypeServiceAccount, serviceAccountGroup, serviceAccountResource, statusSubresource, "1"),
		common.NewFolderTuple("user:17", common.RelationSetView, "4"),
	}

	return setupOpenFGADatabase(t, srv, tuples)
}

func TestMain(m *testing.M) {
	testsuite.Run(m)
}

func TestIntegrationServer(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	// Create a test-specific config to avoid migration conflicts
	cfg := setting.NewCfg()

	// Use a test-specific database to avoid migration conflicts
	testStore := sqlstore.NewTestStore(t, sqlstore.WithCfg(cfg))

	// Hack to skip these tests on mysql 5.7
	if testStore.GetDialect().DriverName() == migrator.MySQL {
		if supported, err := testStore.RecursiveQueriesAreSupported(); !supported || err != nil {
			t.Skip("skipping integration test")
		}
	}

	srv := setupOpenFGAServer(t, testStore, cfg)
	t.Run("test check", func(t *testing.T) {
		setup(t, srv)
		testCheck(t, srv)
	})

	t.Run("test list", func(t *testing.T) {
		setup(t, srv)
		testList(t, srv)
	})

	t.Run("test list streaming", func(t *testing.T) {
		setup(t, srv)
		srv.cfg.UseStreamedListObjects = true
		testList(t, srv)
		srv.cfg.UseStreamedListObjects = false
	})

	t.Run("test batch check", func(t *testing.T) {
		setup(t, srv)
		testBatchCheck(t, srv)
	})

	t.Run("test mutate", func(t *testing.T) {
		testMutate(t, srv)
	})

	t.Run("test mutate folders", func(t *testing.T) {
		testMutateFolders(t, srv)
	})

	t.Run("test mutate resource permissions", func(t *testing.T) {
		testMutateResourcePermissions(t, srv)
	})

	t.Run("test mutate org roles", func(t *testing.T) {
		testMutateOrgRoles(t, srv)
	})

	t.Run("test query folders", func(t *testing.T) {
		testQueryFolders(t, srv)
	})

	t.Run("test mutate role bindings", func(t *testing.T) {
		testMutateRoleBindings(t, srv)
	})

	t.Run("test mutate team bindings", func(t *testing.T) {
		testMutateTeamBindings(t, srv)
	})

	t.Run("test mutate roles", func(t *testing.T) {
		testMutateRoles(t, srv)
	})
}

func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
	t.Helper()

	store, err := store.NewEmbeddedStore(cfg, testDB, log.NewNopLogger())
	require.NoError(t, err)
	openfga, err := NewOpenFGAServer(cfg.ZanzanaServer, store)
	require.NoError(t, err)

	srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger(), tracing.NewNoopTracerService(), prometheus.NewRegistry())
	require.NoError(t, err)

	return srv
}

func setupOpenFGADatabase(t *testing.T, srv *Server, tuples []*openfgav1.TupleKey) *Server {
	t.Helper()

	storeInf, err := srv.getStoreInfo(context.Background(), namespace)
	require.NoError(t, err)

	// Clean up any existing store
	_, err = srv.openfga.DeleteStore(context.Background(), &openfgav1.DeleteStoreRequest{
		StoreId: storeInf.ID,
	})
	require.NoError(t, err)

	// seed tuples
	writes := &openfgav1.WriteRequestWrites{
		TupleKeys:   tuples,
		OnDuplicate: "ignore",
	}

	// First, try to delete any existing tuples to avoid conflicts
	deletes := make([]*openfgav1.TupleKeyWithoutCondition, 0, len(writes.TupleKeys))
	for _, tupleKey := range writes.TupleKeys {
		deletes = append(deletes, &openfgav1.TupleKeyWithoutCondition{
			User:     tupleKey.User,
			Relation: tupleKey.Relation,
			Object:   tupleKey.Object,
		})
	}

	// Try to delete existing tuples (ignore errors if they don't exist)
	_, err = srv.openfga.Write(context.Background(), &openfgav1.WriteRequest{
		StoreId:              storeInf.ID,
		AuthorizationModelId: storeInf.ModelID,
		Deletes: &openfgav1.WriteRequestDeletes{
			TupleKeys: deletes,
			OnMissing: "ignore",
		},
	})
	require.NoError(t, err)

	// Now write the new tuples
	_, err = srv.openfga.Write(context.Background(), &openfgav1.WriteRequest{
		StoreId:              storeInf.ID,
		AuthorizationModelId: storeInf.ModelID,
		Writes:               writes,
	})
	require.NoError(t, err)
	return srv
}

func newContextWithNamespace() context.Context {
	ctx := context.Background()
	ctx = claims.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{
		Rest: authnlib.AccessTokenClaims{
			Namespace: "*",
		},
	}))
	return ctx
}
