// Copyright (c) 2026 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.

package machineset_test

import (
	"context"
	"testing"
	"time"

	"github.com/cosi-project/runtime/pkg/controller"
	"github.com/cosi-project/runtime/pkg/resource"
	"github.com/cosi-project/runtime/pkg/resource/rtestutils"
	"github.com/cosi-project/runtime/pkg/safe"
	"github.com/cosi-project/runtime/pkg/state"
	"github.com/cosi-project/runtime/pkg/state/impl/inmem"
	"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
	"github.com/siderolabs/omni/client/pkg/omni/resources/system"
	"github.com/siderolabs/omni/internal/backend/runtime/omni/controllers/helpers"
	"github.com/siderolabs/omni/internal/backend/runtime/omni/controllers/omni/internal/machineset"
)

var logger *zap.Logger

type runtime struct {
	state.State
}

func (rt *runtime) Create(ctx context.Context, r resource.Resource, _ ...controller.CreateOption) error {
	return rt.State.Create(ctx, r)
}

func (rt *runtime) Destroy(ctx context.Context, r resource.Pointer, _ ...controller.DeleteOption) error {
	return rt.State.Destroy(ctx, r)
}

func (rt *runtime) Modify(ctx context.Context, r resource.Resource, f func(resource.Resource) error, _ ...controller.ModifyOption) error {
	_, err := rt.ModifyWithResult(ctx, r, f)

	return err
}

func (rt *runtime) Update(ctx context.Context, r resource.Resource) error {
	return rt.State.Update(ctx, r)
}

func (rt *runtime) ModifyWithResult(ctx context.Context, r resource.Resource, f func(resource.Resource) error, _ ...controller.ModifyOption) (resource.Resource, error) {
	res, err := rt.UpdateWithConflicts(ctx, r.Metadata(), f)
	if state.IsNotFoundError(err) {
		res = r.DeepCopy()

		if err = f(res); err != nil {
			return nil, err
		}

		return res, rt.State.Create(ctx, res)
	}

	return res, err
}

func (rt *runtime) Teardown(ctx context.Context, r resource.Pointer, _ ...controller.DeleteOption) (bool, error) {
	return rt.State.Teardown(ctx, r)
}

func createRuntime() *runtime {
	state := state.WrapCore(namespaced.NewState(inmem.Build))

	return &runtime{state}
}

// TestCreate runs create once, checks that both cluster machine and cluster machine config status resources were created.
// Validate that both resources have the right data in the spec.
func TestCreate(t *testing.T) {
	rt := createRuntime()

	cluster := omni.NewCluster("test")
	cluster.TypedSpec().Value.KubernetesVersion = "v1.6.2"

	machineSet := omni.NewMachineSet("machineset")
	machineSet.Metadata().Labels().Set(omni.LabelCluster, cluster.Metadata().ID())

	create := machineset.Create{ID: "aa"}

	require := require.New(t)

	ctx := t.Context()

	patch := omni.NewConfigPatch("some")

	err := patch.TypedSpec().Value.SetUncompressedData([]byte(`machine:
  network:
    kubespan:
      enabled: true`))
	require.NoError(err)

	rc, err := machineset.NewReconciliationContext(
		cluster,
		machineSet,
		newHealthyLB(cluster.Metadata().ID()),
		[]*omni.MachineSetNode{
			omni.NewMachineSetNode("aa", machineSet),
		},
		[]*system.ResourceLabels[*omni.MachineStatus]{
			system.NewResourceLabels[*omni.MachineStatus]("aa"),
		},
		nil,
		nil,
		nil,
		nil,
	)
	require.NoError(err)

	clusterMachine := omni.NewClusterMachine("aa")

	helpers.UpdateInputsVersions(clusterMachine, patch)

	require.NoError(create.Apply(ctx, rt, logger, rc))

	clusterMachine, err = safe.ReaderGetByID[*omni.ClusterMachine](ctx, rt, "aa")

	require.NoError(err)

	require.NotEmpty(clusterMachine.TypedSpec().Value.KubernetesVersion)
}

// TestTeardown create 2 cluster machine, destroy with quota 1, first should proceed, second should skip.
func TestTeardown(t *testing.T) {
	rt := createRuntime()

	cluster := omni.NewCluster("test")
	cluster.TypedSpec().Value.KubernetesVersion = "v1.6.3"

	machineSet := omni.NewMachineSet("machineset")
	machineSet.Metadata().Labels().Set(omni.LabelCluster, cluster.Metadata().ID())

	quota := machineset.ChangeQuota{
		Teardown: 1,
	}

	require := require.New(t)

	ctx := t.Context()

	clusterMachines := []*omni.ClusterMachine{
		omni.NewClusterMachine("aa"),
		omni.NewClusterMachine("bb"),
	}

	for _, cm := range clusterMachines {
		cm.Metadata().Labels().Set(omni.LabelCluster, cluster.Metadata().ID())
		cm.Metadata().Labels().Set(omni.LabelMachineSet, machineSet.Metadata().ID())

		require.NoError(rt.Create(ctx, cm))
	}

	rc, err := machineset.NewReconciliationContext(
		cluster,
		machineSet,
		newHealthyLB(cluster.Metadata().ID()),
		[]*omni.MachineSetNode{
			omni.NewMachineSetNode("aa", machineSet),
			omni.NewMachineSetNode("bb", machineSet),
		},
		[]*system.ResourceLabels[*omni.MachineStatus]{
			system.NewResourceLabels[*omni.MachineStatus]("aa"),
			system.NewResourceLabels[*omni.MachineStatus]("bb"),
		},
		clusterMachines,
		nil,
		nil,
		nil,
	)
	require.NoError(err)

	teardown := machineset.Teardown{ID: "aa", Quota: &quota}
	require.NoError(teardown.Apply(ctx, rt, logger, rc))

	require.Equal(0, quota.Teardown)

	teardown = machineset.Teardown{ID: "bb", Quota: &quota}
	require.NoError(teardown.Apply(ctx, rt, logger, rc))

	require.Equal(0, quota.Teardown)

	clusterMachine, err := safe.ReaderGetByID[*omni.ClusterMachine](ctx, rt, "aa")

	require.NoError(err)
	require.Equal(resource.PhaseTearingDown, clusterMachine.Metadata().Phase())

	clusterMachine, err = safe.ReaderGetByID[*omni.ClusterMachine](ctx, rt, "bb")

	require.NoError(err)
	require.Equal(resource.PhaseRunning, clusterMachine.Metadata().Phase())
}

// TestDestroy create tearing down machines, destroy them, should have no resources after the operation is complete.
func TestDestroy(t *testing.T) {
	rt := createRuntime()

	cluster := omni.NewCluster("test")
	cluster.TypedSpec().Value.KubernetesVersion = "v1.6.3"

	machineSet := omni.NewMachineSet("machineset")
	machineSet.Metadata().Labels().Set(omni.LabelCluster, cluster.Metadata().ID())

	require := require.New(t)

	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
	defer cancel()

	clusterMachines := []*omni.ClusterMachine{
		omni.NewClusterMachine("aa"),
		omni.NewClusterMachine("bb"),
	}

	for _, cm := range clusterMachines {
		cm.Metadata().Labels().Set(omni.LabelCluster, cluster.Metadata().ID())
		cm.Metadata().Labels().Set(omni.LabelMachineSet, machineSet.Metadata().ID())

		machine := omni.NewMachine(cm.Metadata().ID())
		machine.Metadata().Finalizers().Add(machineset.ControllerName)

		require.NoError(rt.Create(ctx, cm))
		require.NoError(rt.Create(ctx, machine))

		_, err := rt.Teardown(ctx, cm.Metadata())
		require.NoError(err)

		cm.Metadata().SetPhase(resource.PhaseTearingDown)
	}

	rc, err := machineset.NewReconciliationContext(
		cluster,
		machineSet,
		newHealthyLB(cluster.Metadata().ID()),
		nil,
		nil,
		clusterMachines,
		nil,
		nil,
		nil,
	)
	require.NoError(err)

	destroy := machineset.Destroy{ID: "aa"}
	require.NoError(destroy.Apply(ctx, rt, logger, rc))

	destroy = machineset.Destroy{ID: "bb"}
	require.NoError(destroy.Apply(ctx, rt, logger, rc))

	rtestutils.AssertNoResource[*omni.ClusterMachine](ctx, t, rt.State, "aa")
	rtestutils.AssertNoResource[*omni.ClusterMachine](ctx, t, rt.State, "bb")
	rtestutils.AssertResources(ctx, t, rt.State, []string{"aa", "bb"}, func(r *omni.Machine, assertion *assert.Assertions) {
		assertion.True(r.Metadata().Finalizers().Empty())
	})
}

func init() {
	var err error

	logger, err = zap.NewDevelopment()
	if err != nil {
		panic(err)
	}
}

func init() {
	var err error

	logger, err = zap.NewDevelopment()
	if err != nil {
		panic(err)
	}
}
