package daemon

import (
	"context"
	"isle/bootstrap"
	"isle/daemon/daecommon"
	"isle/daemon/network"
	"isle/toolkit"
	"testing"

	"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type expectNetworkLoad struct {
	creationParams bootstrap.CreationParams
	networkConfig  *daecommon.NetworkConfig
	network        *network.MockNetwork
}

type harnessOpts struct {
	config               daecommon.Config
	expectNetworksLoaded []expectNetworkLoad
}

func (o *harnessOpts) withDefaults() *harnessOpts {
	if o == nil {
		o = new(harnessOpts)
	}
	return o
}

type harness struct {
	ctx           context.Context
	networkLoader *network.MockLoader
	daemon        *Daemon
}

func newHarness(t *testing.T, opts *harnessOpts) *harness {
	t.Parallel()

	opts = opts.withDefaults()

	var (
		ctx           = context.Background()
		logger        = toolkit.NewTestLogger(t)
		networkLoader = network.NewMockLoader(t)
	)

	expectLoadable := make(
		[]bootstrap.CreationParams, len(opts.expectNetworksLoaded),
	)
	for i, expectNetworkLoaded := range opts.expectNetworksLoaded {
		expectLoadable[i] = expectNetworkLoaded.creationParams
		networkLoader.
			On(
				"Load",
				toolkit.MockArg[context.Context](),
				toolkit.MockArg[*mlog.Logger](),
				expectNetworkLoaded.creationParams,
				&network.Opts{
					Config: expectNetworkLoaded.networkConfig,
				},
			).
			Return(expectNetworkLoaded.network, nil).
			Once()

		expectNetworkLoaded.network.On("Shutdown").Return(nil).Once()
	}

	networkLoader.
		On("Loadable", toolkit.MockArg[context.Context]()).
		Return(expectLoadable, nil).
		Once()

	daemon, err := New(ctx, logger, networkLoader, opts.config)
	require.NoError(t, err)

	t.Cleanup(func() {
		t.Log("Shutting down Daemon")
		assert.NoError(t, daemon.Shutdown())
	})

	return &harness{ctx, networkLoader, daemon}
}

func TestNew(t *testing.T) {
	t.Run("no networks loaded", func(t *testing.T) {
		_ = newHarness(t, nil)
	})

	t.Run("DEPRECATED network config matching", func(t *testing.T) {
		var (
			creationParams = bootstrap.NewCreationParams("AAA", "a.com")
			networkConfig  = daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
				c.DNS.Resolvers = []string{"foo"}
			})
			config = daecommon.Config{
				Networks: map[string]daecommon.NetworkConfig{
					daecommon.DeprecatedNetworkID: networkConfig,
				},
			}
		)

		_ = newHarness(t, &harnessOpts{
			config: config,
			expectNetworksLoaded: []expectNetworkLoad{
				{
					creationParams,
					&networkConfig,
					network.NewMockNetwork(t),
				},
			},
		})
	})

	t.Run("network config matching", func(t *testing.T) {
		var (
			creationParamsA = bootstrap.NewCreationParams("AAA", "a.com")
			creationParamsB = bootstrap.NewCreationParams("BBB", "b.com")
			creationParamsC = bootstrap.NewCreationParams("CCC", "c.com")
			creationParamsD = bootstrap.NewCreationParams("DDD", "d.com")

			networkConfigA = daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
				c.DNS.Resolvers = []string{"foo"}
			})

			networkConfigB = daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
				c.VPN.PublicAddr = "1.2.3.4:5"
			})

			networkConfigC = daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
				c.Storage.Allocations = []daecommon.ConfigStorageAllocation{
					{
						DataPath: "/path/data",
						MetaPath: "/path/meta",
						Capacity: 1,
					},
				}
			})

			config = daecommon.Config{
				Networks: map[string]daecommon.NetworkConfig{
					creationParamsA.ID:     networkConfigA,
					creationParamsB.Name:   networkConfigB,
					creationParamsC.Domain: networkConfigC,
					"unknown":              {},
				},
			}
		)

		_ = newHarness(t, &harnessOpts{
			config: config,
			expectNetworksLoaded: []expectNetworkLoad{
				{
					creationParamsA,
					&networkConfigA,
					network.NewMockNetwork(t),
				},
				{
					creationParamsB,
					&networkConfigB,
					network.NewMockNetwork(t),
				},
				{
					creationParamsC,
					&networkConfigC,
					network.NewMockNetwork(t),
				},
				{
					creationParamsD,
					nil,
					network.NewMockNetwork(t),
				},
			},
		})
	})
}

func TestDaemon_SetConfig(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		var (
			networkA = network.NewMockNetwork(t)
			h        = newHarness(t, &harnessOpts{
				expectNetworksLoaded: []expectNetworkLoad{{
					bootstrap.NewCreationParams("AAA", "a.com"), nil, networkA,
				}},
			})

			networkConfig = daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
				c.VPN.PublicAddr = "1.2.3.4:5"
			})
		)

		networkA.
			On("SetConfig", toolkit.MockArg[context.Context](), networkConfig).
			Return(nil).
			Once()

		err := h.daemon.SetConfig(h.ctx, networkConfig)
		assert.NoError(t, err)
	})

	t.Run("ErrManagedNetworkConfig", func(t *testing.T) {
		var (
			creationParams = bootstrap.NewCreationParams("AAA", "a.com")
			networkA       = network.NewMockNetwork(t)
			networkConfig  = daecommon.NewNetworkConfig(nil)

			h = newHarness(t, &harnessOpts{
				config: daecommon.Config{
					Networks: map[string]daecommon.NetworkConfig{
						creationParams.Name: networkConfig,
					},
				},
				expectNetworksLoaded: []expectNetworkLoad{
					{creationParams, &networkConfig, networkA},
				},
			})
		)

		networkConfig.VPN.PublicAddr = "1.2.3.4:5"
		err := h.daemon.SetConfig(h.ctx, networkConfig)
		assert.ErrorIs(t, err, ErrManagedNetworkConfig)
	})
}