// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package config_test

import (
	"net/url"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/cosi-project/runtime/pkg/resource"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	configctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/config"
	"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest"
	talosconfig "github.com/siderolabs/talos/pkg/machinery/config"
	"github.com/siderolabs/talos/pkg/machinery/config/container"
	"github.com/siderolabs/talos/pkg/machinery/config/types/siderolink"
	"github.com/siderolabs/talos/pkg/machinery/constants"
	"github.com/siderolabs/talos/pkg/machinery/resources/block"
	"github.com/siderolabs/talos/pkg/machinery/resources/config"
)

type PersistenceSuite struct {
	ctest.DefaultSuite

	cfg1, cfg2 talosconfig.Provider
}

func (suite *PersistenceSuite) TestPersist() {
	volumeLifecycle := block.NewVolumeLifecycle(block.NamespaceName, block.VolumeLifecycleID)
	suite.Create(volumeLifecycle)

	ctest.AssertResource(suite, block.VolumeLifecycleID, func(vl *block.VolumeLifecycle, asrt *assert.Assertions) {
		asrt.False(vl.Metadata().Finalizers().Empty())
	})

	statePath := suite.T().TempDir()
	mountID := (&configctrl.PersistenceController{}).Name() + "-" + constants.StatePartitionLabel

	ctest.AssertNoResource[*block.VolumeMountRequest](suite, mountID)

	c1 := config.NewMachineConfigWithID(suite.cfg1, config.PersistentID)
	suite.Create(c1)

	ctest.AssertResource(suite, mountID, func(mountRequest *block.VolumeMountRequest, asrt *assert.Assertions) {
		asrt.Equal(constants.StatePartitionLabel, mountRequest.TypedSpec().VolumeID)
	})

	volumeMountStatus := block.NewVolumeMountStatus(block.NamespaceName, mountID)
	volumeMountStatus.TypedSpec().Target = statePath
	suite.Create(volumeMountStatus)

	suite.EventuallyWithT(func(collect *assert.CollectT) {
		asrt := assert.New(collect)

		asrt.FileExists(filepath.Join(statePath, constants.ConfigFilename))
	}, time.Second, 10*time.Millisecond)

	ctest.AssertResources(suite, []resource.ID{volumeMountStatus.Metadata().ID()}, func(vms *block.VolumeMountStatus, asrt *assert.Assertions) {
		asrt.True(vms.Metadata().Finalizers().Empty())
	})

	suite.Destroy(volumeMountStatus)

	ctest.AssertNoResource[*block.VolumeMountRequest](suite, mountID)

	c2 := config.NewMachineConfigWithID(suite.cfg2, config.PersistentID)
	c2.Metadata().SetVersion(c1.Metadata().Version())
	suite.Update(c2)

	ctest.AssertResource(suite, mountID, func(mountRequest *block.VolumeMountRequest, asrt *assert.Assertions) {
		asrt.Equal(constants.StatePartitionLabel, mountRequest.TypedSpec().VolumeID)
	})

	// teardown the volume lifecycle, but finalizer should not be removed yet
	_, err := suite.State().Teardown(suite.Ctx(), volumeLifecycle.Metadata())
	suite.Require().NoError(err)

	ctest.AssertResource(suite, block.VolumeLifecycleID, func(vl *block.VolumeLifecycle, asrt *assert.Assertions) {
		asrt.False(vl.Metadata().Finalizers().Empty())
	})

	volumeMountStatus = block.NewVolumeMountStatus(block.NamespaceName, mountID)
	volumeMountStatus.TypedSpec().Target = statePath
	suite.Create(volumeMountStatus)

	suite.EventuallyWithT(func(collect *assert.CollectT) {
		asrt := assert.New(collect)

		contents, err := os.ReadFile(filepath.Join(statePath, constants.ConfigFilename))
		asrt.NoError(err)

		asrt.Contains(string(contents), "jointoken=none")
	}, time.Second, 10*time.Millisecond)

	ctest.AssertResources(suite, []resource.ID{volumeMountStatus.Metadata().ID()}, func(vms *block.VolumeMountStatus, asrt *assert.Assertions) {
		asrt.True(vms.Metadata().Finalizers().Empty())
	})

	suite.Destroy(volumeMountStatus)

	ctest.AssertResource(suite, block.VolumeLifecycleID, func(vl *block.VolumeLifecycle, asrt *assert.Assertions) {
		asrt.True(vl.Metadata().Finalizers().Empty())
	})

	suite.Destroy(volumeLifecycle)
}

func (suite *PersistenceSuite) TestConfig() {
	volumeLifecycle := block.NewVolumeLifecycle(block.NamespaceName, block.VolumeLifecycleID)
	suite.Create(volumeLifecycle)

	ctest.AssertResource(suite, block.VolumeLifecycleID, func(vl *block.VolumeLifecycle, asrt *assert.Assertions) {
		asrt.False(vl.Metadata().Finalizers().Empty())
	})

	_, err := suite.State().Teardown(suite.Ctx(), volumeLifecycle.Metadata())
	suite.Require().NoError(err)

	ctest.AssertResource(suite, block.VolumeLifecycleID, func(vl *block.VolumeLifecycle, asrt *assert.Assertions) {
		asrt.True(vl.Metadata().Finalizers().Empty())
	})

	suite.Destroy(volumeLifecycle)
}

func (suite *PersistenceSuite) TestNoPersistenceWithMissingState() {
	volumeLifecycle := block.NewVolumeLifecycle(block.NamespaceName, block.VolumeLifecycleID)
	suite.Create(volumeLifecycle)

	ctest.AssertResource(suite, block.VolumeLifecycleID, func(vl *block.VolumeLifecycle, asrt *assert.Assertions) {
		asrt.False(vl.Metadata().Finalizers().Empty())
	})

	c1 := config.NewMachineConfigWithID(suite.cfg1, config.PersistentID)
	suite.Create(c1)

	_, err := suite.State().Teardown(suite.Ctx(), volumeLifecycle.Metadata())
	suite.Require().NoError(err)

	ctest.AssertResource(suite, block.VolumeLifecycleID, func(vl *block.VolumeLifecycle, asrt *assert.Assertions) {
		asrt.False(vl.Metadata().Finalizers().Empty())
	})

	// simulate STATE missing
	volumeStatus := block.NewVolumeStatus(block.NamespaceName, constants.StatePartitionLabel)
	volumeStatus.TypedSpec().Phase = block.VolumePhaseMissing
	suite.Create(volumeStatus)

	ctest.AssertResource(suite, block.VolumeLifecycleID, func(vl *block.VolumeLifecycle, asrt *assert.Assertions) {
		asrt.True(vl.Metadata().Finalizers().Empty())
	})

	suite.Destroy(volumeLifecycle)
}

func TestPersistenceSuite(t *testing.T) {
	t.Parallel()

	if os.Geteuid() != 0 {
		t.Skip("skipping test that requires root privileges")
	}

	sideroLinkCfg1 := siderolink.NewConfigV1Alpha1()
	sideroLinkCfg1.APIUrlConfig.URL = must(url.Parse("https://siderolink.api/?jointoken=secret&user=alice"))

	cfg1, err := container.New(sideroLinkCfg1)
	require.NoError(t, err)

	sideroLinkCfg2 := siderolink.NewConfigV1Alpha1()
	sideroLinkCfg2.APIUrlConfig.URL = must(url.Parse("https://siderolink.api/?jointoken=none&user=bob"))

	cfg2, err := container.New(sideroLinkCfg2)
	require.NoError(t, err)

	suite.Run(t, &PersistenceSuite{
		DefaultSuite: ctest.DefaultSuite{
			AfterSetup: func(suite *ctest.DefaultSuite) {
				suite.Require().NoError(suite.Runtime().RegisterController(&configctrl.PersistenceController{}))
			},
		},

		cfg1: cfg1,
		cfg2: cfg2,
	})
}
