// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package reaper

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
	"github.com/hashicorp/consul/internal/controller"
	"github.com/hashicorp/consul/internal/resource"
	"github.com/hashicorp/consul/internal/resource/demo"
	"github.com/hashicorp/consul/internal/resource/resourcetest"
	rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
	"github.com/hashicorp/consul/proto-public/pbresource"
	"github.com/hashicorp/consul/sdk/testutil"
)

func TestReconcile_ResourceWithNoChildren(t *testing.T) {
	client := setupResourceService(t)
	runReaperTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
		// Seed the database with an artist.
		res, err := demo.GenerateV2Artist()

		// set resource tenancy from default to test tenancy
		res.Id.Tenancy = tenancy

		require.NoError(t, err)
		ctx := testutil.TestContext(t)
		writeRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: res})
		require.NoError(t, err)

		// Delete the artist to create a tombstone
		_, err = client.Delete(ctx, &pbresource.DeleteRequest{Id: writeRsp.Resource.Id})
		require.NoError(t, err)

		// Retrieve tombstone
		listRsp, err := client.List(ctx, &pbresource.ListRequest{
			Type:    resource.TypeV1Tombstone,
			Tenancy: tenancy,
		})
		require.NoError(t, err)
		require.Len(t, listRsp.Resources, 1)
		tombstone := listRsp.Resources[0]

		// Verify reconcile does first pass and queues up for a second pass
		rec := newReconciler()
		runtime := controller.Runtime{
			Client: client,
			Logger: testutil.Logger(t),
		}
		req := controller.Request{ID: tombstone.Id}
		require.ErrorIs(t, controller.RequeueAfterError(secondPassDelay), rec.Reconcile(ctx, runtime, req))

		// Verify condition FirstPassCompleted is true
		readRsp, err := client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
		require.NoError(t, err)
		tombstone = readRsp.Resource
		condition := tombstone.Status[statusKeyReaperController].Conditions[0]
		require.Equal(t, conditionTypeFirstPassCompleted, condition.Type)
		require.Equal(t, pbresource.Condition_STATE_TRUE, condition.State)

		// Verify reconcile does second pass and tombstone is deleted
		// Fake out time so elapsed time > secondPassDelay
		rec.timeNow = func() time.Time { return time.Now().Add(secondPassDelay + time.Second) }
		require.NoError(t, rec.Reconcile(ctx, runtime, req))
		_, err = client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
		require.Error(t, err)
		require.Equal(t, codes.NotFound.String(), status.Code(err).String())

		// Reconcile again to verify no-op on an already deleted tombstone
		require.NoError(t, rec.Reconcile(ctx, runtime, req))
	})
}

func TestReconcile_ResourceWithChildren(t *testing.T) {
	client := setupResourceService(t)
	runReaperTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
		// Seed the database with an artist
		res, err := demo.GenerateV2Artist()

		// set resource tenancy from default to test tenancy
		res.Id.Tenancy = tenancy

		require.NoError(t, err)
		ctx := testutil.TestContext(t)
		writeRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: res})
		require.NoError(t, err)
		artist := writeRsp.Resource

		// Create 3 albums owned by the artist
		numAlbums := 3
		for i := 0; i < numAlbums; i++ {
			res, err = demo.GenerateV2Album(artist.Id)
			require.NoError(t, err)
			_, err := client.Write(ctx, &pbresource.WriteRequest{Resource: res})
			require.NoError(t, err)
		}

		// Delete the artist to create a tombstone
		_, err = client.Delete(ctx, &pbresource.DeleteRequest{Id: writeRsp.Resource.Id})
		require.NoError(t, err)

		// Retrieve the tombstone
		listRsp, err := client.List(ctx, &pbresource.ListRequest{
			Type:    resource.TypeV1Tombstone,
			Tenancy: writeRsp.Resource.Id.Tenancy,
		})
		require.NoError(t, err)
		require.Len(t, listRsp.Resources, 1)
		tombstone := listRsp.Resources[0]

		// Verify reconcile does first pass delete and queues up for a second pass
		rec := newReconciler()
		runtime := controller.Runtime{
			Client: client,
			Logger: testutil.Logger(t),
		}
		req := controller.Request{ID: tombstone.Id}
		require.ErrorIs(t, controller.RequeueAfterError(secondPassDelay), rec.Reconcile(ctx, runtime, req))

		// Verify 3 albums deleted
		listRsp, err = client.List(ctx, &pbresource.ListRequest{
			Type:    demo.TypeV2Album,
			Tenancy: artist.Id.Tenancy,
		})
		require.NoError(t, err)
		require.Empty(t, listRsp.Resources)

		// Verify condition FirstPassCompleted is true
		readRsp, err := client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
		require.NoError(t, err)
		tombstone = readRsp.Resource
		condition := tombstone.Status[statusKeyReaperController].Conditions[0]
		require.Equal(t, conditionTypeFirstPassCompleted, condition.Type)
		require.Equal(t, pbresource.Condition_STATE_TRUE, condition.State)

		// Verify reconcile does second pass
		// Fake out time so elapsed time > secondPassDelay
		rec.timeNow = func() time.Time { return time.Now().Add(secondPassDelay + time.Second) }
		require.NoError(t, rec.Reconcile(ctx, runtime, req))

		// Verify artist tombstone deleted
		_, err = client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
		require.Error(t, err)
		require.Equal(t, codes.NotFound.String(), status.Code(err).String())

		// Verify tombstones for 3 albums created
		listRsp, err = client.List(ctx, &pbresource.ListRequest{
			Type:    resource.TypeV1Tombstone,
			Tenancy: artist.Id.Tenancy,
		})
		require.NoError(t, err)
		require.Len(t, listRsp.Resources, numAlbums)
	})
}

func TestReconcile_RequeueWithDelayWhenSecondPassDelayNotElapsed(t *testing.T) {
	client := setupResourceService(t)
	runReaperTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
		// Seed the database with an artist.
		res, err := demo.GenerateV2Artist()

		// set resource tenancy from default to test tenancy
		res.Id.Tenancy = tenancy

		require.NoError(t, err)
		ctx := testutil.TestContext(t)
		writeRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: res})
		require.NoError(t, err)

		// Delete the artist to create a tombstone
		_, err = client.Delete(ctx, &pbresource.DeleteRequest{Id: writeRsp.Resource.Id})
		require.NoError(t, err)

		// Retrieve tombstone
		listRsp, err := client.List(ctx, &pbresource.ListRequest{
			Type:    resource.TypeV1Tombstone,
			Tenancy: writeRsp.Resource.Id.Tenancy,
		})
		require.NoError(t, err)
		require.Len(t, listRsp.Resources, 1)
		tombstone := listRsp.Resources[0]

		// Verify reconcile does first pass and queues up for a second pass
		rec := newReconciler()
		runtime := controller.Runtime{
			Client: client,
			Logger: testutil.Logger(t),
		}
		req := controller.Request{ID: tombstone.Id}
		require.ErrorIs(t, controller.RequeueAfterError(secondPassDelay), rec.Reconcile(ctx, runtime, req))

		// Verify condition FirstPassCompleted is true
		readRsp, err := client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
		require.NoError(t, err)
		tombstone = readRsp.Resource
		condition := tombstone.Status[statusKeyReaperController].Conditions[0]
		require.Equal(t, conditionTypeFirstPassCompleted, condition.Type)
		require.Equal(t, pbresource.Condition_STATE_TRUE, condition.State)

		// Verify requeued for second pass since secondPassDelay time has not elapsed
		require.ErrorIs(t, controller.RequeueAfterError(secondPassDelay), rec.Reconcile(ctx, runtime, req))
	})
}

func runReaperTestCaseWithTenancies(testCase func(tenancy *pbresource.Tenancy)) {
	for _, tenancy := range resourcetest.TestTenancies() {
		testCase(tenancy)
	}
}

func setupResourceService(t *testing.T) pbresource.ResourceServiceClient {
	return svctest.NewResourceServiceBuilder().
		WithTenancies(rtest.TestTenancies()...).
		WithRegisterFns(demo.RegisterTypes).
		Run(t)
}
