// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package client

import (
	"fmt"
	"testing"
	"time"

	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/client/config"
	"github.com/hashicorp/nomad/client/pluginmanager/drivermanager"
	"github.com/hashicorp/nomad/helper/pluginutils/catalog"
	nconfig "github.com/hashicorp/nomad/nomad/structs/config"
	"github.com/hashicorp/nomad/testutil"
	"github.com/stretchr/testify/require"
)

// TestDriverManager_Fingerprint_Run asserts that node is populated with
// driver fingerprints
func TestDriverManager_Fingerprint_Run(t *testing.T) {
	ci.Parallel(t)

	testClient, cleanup := TestClient(t, nil)
	defer cleanup()

	conf := testClient.GetConfig()
	dm := drivermanager.New(&drivermanager.Config{
		Logger:              testClient.logger,
		Loader:              conf.PluginSingletonLoader,
		PluginConfig:        conf.NomadPluginConfig(),
		Updater:             testClient.updateNodeFromDriver,
		EventHandlerFactory: testClient.GetTaskEventHandler,
		State:               testClient.stateDB,
	})

	go dm.Run()
	defer dm.Shutdown()

	testutil.WaitForResult(func() (bool, error) {
		node := testClient.Node()

		d, ok := node.Drivers["mock_driver"]
		if !ok {
			return false, fmt.Errorf("mock_driver driver is not present: %+v", node.Drivers)
		}

		if !d.Detected || !d.Healthy {
			return false, fmt.Errorf("mock_driver driver is not marked healthy: %+v", d)
		}

		return true, nil
	}, func(err error) {
		require.NoError(t, err)
	})
}

// TestDriverManager_Fingerprint_Run asserts that node is populated with
// driver fingerprints and it's updated periodically
func TestDriverManager_Fingerprint_Periodic(t *testing.T) {
	ci.Parallel(t)

	testClient, cleanup := TestClient(t, func(c *config.Config) {
		pluginConfig := []*nconfig.PluginConfig{
			{
				Name: "mock_driver",
				Config: map[string]interface{}{
					"shutdown_periodic_after":    true,
					"shutdown_periodic_duration": 2 * time.Second,
				},
			},
		}

		c.PluginLoader = catalog.TestPluginLoaderWithOptions(t, "", map[string]string{}, pluginConfig)

	})
	defer cleanup()

	conf := testClient.GetConfig()
	dm := drivermanager.New(&drivermanager.Config{
		Logger:              testClient.logger,
		Loader:              conf.PluginSingletonLoader,
		PluginConfig:        conf.NomadPluginConfig(),
		Updater:             testClient.updateNodeFromDriver,
		EventHandlerFactory: testClient.GetTaskEventHandler,
		State:               testClient.stateDB,
	})

	go dm.Run()
	defer dm.Shutdown()

	// we get a healthy mock_driver first
	testutil.WaitForResult(func() (bool, error) {
		node := testClient.Node()

		d, ok := node.Drivers["mock_driver"]
		if !ok {
			return false, fmt.Errorf("mock_driver driver is not present: %+v", node.Drivers)
		}

		if !d.Detected || !d.Healthy {
			return false, fmt.Errorf("mock_driver driver is not marked healthy: %+v", d)
		}

		return true, nil
	}, func(err error) {
		require.NoError(t, err)
	})

	// eventually, the mock_driver is marked as unhealthy
	testutil.WaitForResult(func() (bool, error) {
		node := testClient.Node()

		d, ok := node.Drivers["mock_driver"]
		if !ok {
			return false, fmt.Errorf("mock_driver driver is not present: %+v", node.Drivers)
		}

		if d.Detected || d.Healthy {
			return false, fmt.Errorf("mock_driver driver is still marked as healthy: %+v", d)
		}

		return true, nil
	}, func(err error) {
		require.NoError(t, err)
	})
}

// TestDriverManager_NodeAttributes_Run asserts that node attributes are populated
// in addition to node.Drivers until we fully deprecate it
func TestDriverManager_NodeAttributes_Run(t *testing.T) {
	ci.Parallel(t)

	testClient, cleanup := TestClient(t, func(c *config.Config) {
		c.Options = map[string]string{
			"driver.raw_exec.enable": "1",
		}
	})
	defer cleanup()

	conf := testClient.GetConfig()
	dm := drivermanager.New(&drivermanager.Config{
		Logger:              testClient.logger,
		Loader:              conf.PluginSingletonLoader,
		PluginConfig:        conf.NomadPluginConfig(),
		Updater:             testClient.updateNodeFromDriver,
		EventHandlerFactory: testClient.GetTaskEventHandler,
		State:               testClient.stateDB,
	})

	go dm.Run()
	defer dm.Shutdown()

	// we should have mock_driver as well as raw_exec in node attributes
	testutil.WaitForResult(func() (bool, error) {
		node := testClient.Node()

		// check mock driver
		if node.Attributes["driver.mock_driver"] == "" {
			return false, fmt.Errorf("mock_driver is not present in attributes: %#v", node.Attributes)
		}
		d, ok := node.Drivers["mock_driver"]
		if !ok {
			return false, fmt.Errorf("mock_driver is not present in drivers: %#v", node.Drivers)
		}

		if !d.Detected || !d.Healthy {
			return false, fmt.Errorf("mock_driver driver is not marked as healthy: %+v", d)
		}

		if d.Attributes["driver.mock_driver"] != "" {
			return false, fmt.Errorf("mock driver driver attributes contain duplicate health info: %#v", d.Attributes)
		}

		// check raw_exec
		if node.Attributes["driver.raw_exec"] == "" {
			return false, fmt.Errorf("raw_exec is not present in attributes: %#v", node.Attributes)
		}
		d, ok = node.Drivers["raw_exec"]
		if !ok {
			return false, fmt.Errorf("raw_exec is not present in drivers: %#v", node.Drivers)
		}

		if !d.Detected || !d.Healthy {
			return false, fmt.Errorf("raw_exec driver is not marked as healthy: %+v", d)
		}

		return true, nil
	}, func(err error) {
		require.NoError(t, err)
	})
}
