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

package testutils

import (
	"bytes"
	"context"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/hashicorp/go-msgpack/codec"
	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/plugins/drivers"
	pstructs "github.com/hashicorp/nomad/plugins/shared/structs"
	"github.com/stretchr/testify/require"
)

var _ drivers.DriverPlugin = (*MockDriver)(nil)

// Very simple test to ensure the test harness works as expected
func TestDriverHarness(t *testing.T) {
	ci.Parallel(t)

	handle := &drivers.TaskHandle{Config: &drivers.TaskConfig{Name: "mock"}}
	d := &MockDriver{
		StartTaskF: func(task *drivers.TaskConfig) (*drivers.TaskHandle, *drivers.DriverNetwork, error) {
			return handle, nil, nil
		},
	}
	harness := NewDriverHarness(t, d)
	defer harness.Kill()
	actual, _, err := harness.StartTask(&drivers.TaskConfig{})
	require.NoError(t, err)
	require.Equal(t, handle.Config.Name, actual.Config.Name)
}

type testDriverState struct {
	Pid int
	Log string
}

func TestBaseDriver_Fingerprint(t *testing.T) {
	ci.Parallel(t)
	require := require.New(t)

	fingerprints := []*drivers.Fingerprint{
		{
			Attributes:        map[string]*pstructs.Attribute{"foo": pstructs.NewStringAttribute("bar")},
			Health:            drivers.HealthStateUnhealthy,
			HealthDescription: "starting up",
		},
		{
			Attributes:        map[string]*pstructs.Attribute{"foo": pstructs.NewStringAttribute("bar")},
			Health:            drivers.HealthStateHealthy,
			HealthDescription: "running",
		},
	}

	var complete atomic.Value
	complete.Store(false)

	impl := &MockDriver{
		FingerprintF: func(ctx context.Context) (<-chan *drivers.Fingerprint, error) {
			ch := make(chan *drivers.Fingerprint)
			go func() {
				defer close(ch)
				ch <- fingerprints[0]
				time.Sleep(500 * time.Millisecond)
				ch <- fingerprints[1]
				complete.Store(true)
			}()
			return ch, nil
		},
	}

	harness := NewDriverHarness(t, impl)
	defer harness.Kill()

	ch, err := harness.Fingerprint(context.Background())
	require.NoError(err)

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		select {
		case f := <-ch:
			require.Exactly(f, fingerprints[0])
		case <-time.After(1 * time.Second):
			require.Fail("did not receive fingerprint[0]")
		}
		select {
		case f := <-ch:
			require.Exactly(f, fingerprints[1])
		case <-time.After(1 * time.Second):
			require.Fail("did not receive fingerprint[1]")
		}
	}()
	require.False(complete.Load().(bool))
	wg.Wait()
	require.True(complete.Load().(bool))
}

func TestBaseDriver_RecoverTask(t *testing.T) {
	ci.Parallel(t)
	require := require.New(t)

	// build driver state and encode it into proto msg
	state := testDriverState{Pid: 1, Log: "foo"}
	var buf bytes.Buffer
	enc := codec.NewEncoder(&buf, structs.MsgpackHandle)
	enc.Encode(state)

	// mock the RecoverTask driver call
	impl := &MockDriver{
		RecoverTaskF: func(h *drivers.TaskHandle) error {
			var actual testDriverState
			require.NoError(h.GetDriverState(&actual))
			require.Equal(state, actual)
			return nil
		},
	}

	harness := NewDriverHarness(t, impl)
	defer harness.Kill()

	handle := &drivers.TaskHandle{
		DriverState: buf.Bytes(),
	}
	err := harness.RecoverTask(handle)
	require.NoError(err)
}

func TestBaseDriver_StartTask(t *testing.T) {
	ci.Parallel(t)
	require := require.New(t)

	cfg := &drivers.TaskConfig{
		ID: "foo",
	}
	state := &testDriverState{Pid: 1, Log: "log"}
	var handle *drivers.TaskHandle
	impl := &MockDriver{
		StartTaskF: func(c *drivers.TaskConfig) (*drivers.TaskHandle, *drivers.DriverNetwork, error) {
			handle = drivers.NewTaskHandle(1)
			handle.Config = c
			handle.State = drivers.TaskStateRunning
			handle.SetDriverState(state)
			return handle, nil, nil
		},
	}

	harness := NewDriverHarness(t, impl)
	defer harness.Kill()
	resp, _, err := harness.StartTask(cfg)
	require.NoError(err)
	require.Equal(cfg.ID, resp.Config.ID)
	require.Equal(handle.State, resp.State)

	var actualState testDriverState
	require.NoError(resp.GetDriverState(&actualState))
	require.Equal(*state, actualState)

}

func TestBaseDriver_WaitTask(t *testing.T) {
	ci.Parallel(t)
	require := require.New(t)

	result := &drivers.ExitResult{ExitCode: 1, Signal: 9}

	signalTask := make(chan struct{})

	impl := &MockDriver{
		WaitTaskF: func(_ context.Context, id string) (<-chan *drivers.ExitResult, error) {
			ch := make(chan *drivers.ExitResult)
			go func() {
				<-signalTask
				ch <- result
			}()
			return ch, nil
		},
	}

	harness := NewDriverHarness(t, impl)
	defer harness.Kill()
	var wg sync.WaitGroup
	wg.Add(1)
	var finished bool
	go func() {
		defer wg.Done()
		ch, err := harness.WaitTask(context.TODO(), "foo")
		require.NoError(err)
		actualResult := <-ch
		finished = true
		require.Exactly(result, actualResult)
	}()
	require.False(finished)
	close(signalTask)
	wg.Wait()
	require.True(finished)
}

func TestBaseDriver_TaskEvents(t *testing.T) {
	ci.Parallel(t)
	require := require.New(t)

	now := time.Now().UTC().Truncate(time.Millisecond)
	events := []*drivers.TaskEvent{
		{
			TaskID:      "abc",
			Timestamp:   now,
			Annotations: map[string]string{"foo": "bar"},
			Message:     "starting",
		},
		{
			TaskID:      "xyz",
			Timestamp:   now.Add(2 * time.Second),
			Annotations: map[string]string{"foo": "bar"},
			Message:     "starting",
		},
		{
			TaskID:      "xyz",
			Timestamp:   now.Add(3 * time.Second),
			Annotations: map[string]string{"foo": "bar"},
			Message:     "running",
		},
		{
			TaskID:      "abc",
			Timestamp:   now.Add(4 * time.Second),
			Annotations: map[string]string{"foo": "bar"},
			Message:     "running",
		},
	}

	impl := &MockDriver{
		TaskEventsF: func(ctx context.Context) (<-chan *drivers.TaskEvent, error) {
			ch := make(chan *drivers.TaskEvent)
			go func() {
				defer close(ch)
				for _, event := range events {
					ch <- event
				}
			}()
			return ch, nil
		},
	}

	harness := NewDriverHarness(t, impl)
	defer harness.Kill()

	ch, err := harness.TaskEvents(context.Background())
	require.NoError(err)

	for _, event := range events {
		select {
		case actual := <-ch:
			require.Exactly(actual, event)
		case <-time.After(500 * time.Millisecond):
			require.Fail("failed to receive event")

		}
	}

}

func TestBaseDriver_Capabilities(t *testing.T) {
	ci.Parallel(t)

	capabilities := &drivers.Capabilities{
		NetIsolationModes: []drivers.NetIsolationMode{
			drivers.NetIsolationModeHost,
			drivers.NetIsolationModeGroup,
		},
		MustInitiateNetwork: true,
		SendSignals:         true,
		Exec:                true,
		FSIsolation:         drivers.FSIsolationNone,
	}
	d := &MockDriver{
		CapabilitiesF: func() (*drivers.Capabilities, error) {
			return capabilities, nil
		},
	}

	harness := NewDriverHarness(t, d)
	defer harness.Kill()

	caps, err := harness.Capabilities()
	require.NoError(t, err)
	require.Equal(t, capabilities, caps)
}
