package service

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/docker/cli/opts"
	"github.com/moby/moby/api/types/container"
	"github.com/moby/moby/api/types/network"
	"github.com/moby/moby/api/types/swarm"
	"github.com/moby/moby/client"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

func TestCredentialSpecOpt(t *testing.T) {
	tests := []struct {
		name        string
		in          string
		value       swarm.CredentialSpec
		expectedErr string
	}{
		{
			name:  "empty",
			in:    "",
			value: swarm.CredentialSpec{},
		},
		{
			name:        "no-prefix",
			in:          "noprefix",
			value:       swarm.CredentialSpec{},
			expectedErr: `invalid credential spec: value must be prefixed with "config://", "file://", or "registry://"`,
		},
		{
			name:  "config",
			in:    "config://0bt9dmxjvjiqermk6xrop3ekq",
			value: swarm.CredentialSpec{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
		},
		{
			name:  "file",
			in:    "file://somefile.json",
			value: swarm.CredentialSpec{File: "somefile.json"},
		},
		{
			name:  "registry",
			in:    "registry://testing",
			value: swarm.CredentialSpec{Registry: "testing"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			var cs credentialSpecOpt

			err := cs.Set(tc.in)

			if tc.expectedErr != "" {
				assert.Error(t, err, tc.expectedErr)
			} else {
				assert.NilError(t, err)
			}

			assert.Equal(t, cs.String(), tc.in)
			assert.DeepEqual(t, cs.Value(), &tc.value)
		})
	}
}

func TestMemBytesString(t *testing.T) {
	var mem opts.MemBytes = 1048576
	assert.Check(t, is.Equal("1MiB", mem.String()))
}

func TestMemBytesSetAndValue(t *testing.T) {
	var mem opts.MemBytes
	assert.NilError(t, mem.Set("5kb"))
	assert.Check(t, is.Equal(int64(5120), mem.Value()))
}

func TestNanoCPUsString(t *testing.T) {
	var cpus opts.NanoCPUs = 6100000000
	assert.Check(t, is.Equal("6.100", cpus.String()))
}

func TestNanoCPUsSetAndValue(t *testing.T) {
	var cpus opts.NanoCPUs
	assert.NilError(t, cpus.Set("0.35"))
	assert.Check(t, is.Equal(int64(350000000), cpus.Value()))
}

func TestUint64OptString(t *testing.T) {
	value := uint64(2345678)
	opt := Uint64Opt{value: &value}
	assert.Check(t, is.Equal("2345678", opt.String()))

	opt = Uint64Opt{}
	assert.Check(t, is.Equal("", opt.String()))
}

func TestUint64OptSetAndValue(t *testing.T) {
	var opt Uint64Opt
	assert.NilError(t, opt.Set("14445"))
	assert.Check(t, is.Equal(uint64(14445), *opt.Value()))
}

func TestHealthCheckOptionsToHealthConfig(t *testing.T) {
	dur := time.Second
	opt := healthCheckOptions{
		cmd:           "curl",
		interval:      opts.PositiveDurationOpt{DurationOpt: *opts.NewDurationOpt(&dur)},
		timeout:       opts.PositiveDurationOpt{DurationOpt: *opts.NewDurationOpt(&dur)},
		startPeriod:   opts.PositiveDurationOpt{DurationOpt: *opts.NewDurationOpt(&dur)},
		startInterval: opts.PositiveDurationOpt{DurationOpt: *opts.NewDurationOpt(&dur)},
		retries:       10,
	}
	config, err := opt.toHealthConfig()
	assert.NilError(t, err)
	assert.Check(t, is.DeepEqual(&container.HealthConfig{
		Test:          []string{"CMD-SHELL", "curl"},
		Interval:      time.Second,
		Timeout:       time.Second,
		StartPeriod:   time.Second,
		StartInterval: time.Second,
		Retries:       10,
	}, config))
}

func TestHealthCheckOptionsToHealthConfigNoHealthcheck(t *testing.T) {
	opt := healthCheckOptions{
		noHealthcheck: true,
	}
	config, err := opt.toHealthConfig()
	assert.NilError(t, err)
	assert.Check(t, is.DeepEqual(&container.HealthConfig{
		Test: []string{"NONE"},
	}, config))
}

func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) {
	opt := healthCheckOptions{
		cmd:           "curl",
		noHealthcheck: true,
	}
	_, err := opt.toHealthConfig()
	assert.Error(t, err, "--no-healthcheck conflicts with --health-* options")
}

func TestResourceOptionsToResourceRequirements(t *testing.T) {
	incorrectOptions := []resourceOptions{
		{
			resGenericResources: []string{"foo=bar", "foo=1"},
		},
		{
			resGenericResources: []string{"foo=bar", "foo=baz"},
		},
		{
			resGenericResources: []string{"foo=bar"},
		},
		{
			resGenericResources: []string{"foo=1", "foo=2"},
		},
	}

	flags := newCreateCommand(nil).Flags()

	for _, opt := range incorrectOptions {
		_, err := opt.ToResourceRequirements(flags)
		assert.Check(t, is.ErrorContains(err, ""))
	}

	correctOptions := []resourceOptions{
		{
			resGenericResources: []string{"foo=1"},
		},
		{
			resGenericResources: []string{"foo=1", "bar=2"},
		},
	}

	for _, opt := range correctOptions {
		r, err := opt.ToResourceRequirements(flags)
		assert.NilError(t, err)
		assert.Check(t, is.Len(r.Reservations.GenericResources, len(opt.resGenericResources)))
	}
}

func TestResourceOptionsToResourceRequirementsSwap(t *testing.T) {
	// first, check that no flag set means no field set in the return
	flags := newCreateCommand(nil).Flags()

	// These should be the default values of the field.
	swapOptions := resourceOptions{
		swapBytes:     0,
		memSwappiness: -1,
	}

	r, err := swapOptions.ToResourceRequirements(flags)
	assert.NilError(t, err)
	assert.Check(t, is.Nil(r.SwapBytes))
	assert.Check(t, is.Nil(r.MemorySwappiness))

	// now set the flags and some values
	flags.Set(flagSwapBytes, "86000")
	flags.Set(flagMemSwappiness, "23")
	swapOptions.swapBytes = 86000
	swapOptions.memSwappiness = 23

	r, err = swapOptions.ToResourceRequirements(flags)
	assert.NilError(t, err)
	assert.Check(t, r.SwapBytes != nil)
	assert.Check(t, is.Equal(*(r.SwapBytes), int64(86000)))
	assert.Check(t, r.MemorySwappiness != nil)
	assert.Check(t, is.Equal(*(r.MemorySwappiness), int64(23)))
}

func TestToServiceNetwork(t *testing.T) {
	nws := []network.Inspect{
		{
			Network: network.Network{
				Name: "aaa-network",
				ID:   "id555",
			},
		},
		{
			Network: network.Network{
				Name: "mmm-network",
				ID:   "id999",
			},
		},
		{
			Network: network.Network{
				Name: "zzz-network",
				ID:   "id111",
			},
		},
	}

	apiClient := &fakeClient{
		networkInspectFunc: func(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
			for _, nw := range nws {
				if nw.ID == networkID || nw.Name == networkID {
					return client.NetworkInspectResult{Network: nw}, nil
				}
			}
			return client.NetworkInspectResult{}, fmt.Errorf("network not found: %s", networkID)
		},
	}

	nwo := opts.NetworkOpt{}
	assert.NilError(t, nwo.Set("zzz-network"))
	assert.NilError(t, nwo.Set("mmm-network"))
	assert.NilError(t, nwo.Set("aaa-network"))

	o := newServiceOptions()
	o.mode = "replicated"
	o.networks = nwo

	ctx := context.Background()
	flags := newCreateCommand(nil).Flags()
	service, err := o.ToService(ctx, apiClient, flags)
	assert.NilError(t, err)
	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id111"}, {Target: "id555"}, {Target: "id999"}}, service.TaskTemplate.Networks))
}

func TestToServicePidsLimit(t *testing.T) {
	flags := newCreateCommand(nil).Flags()
	opt := newServiceOptions()
	opt.mode = "replicated"
	opt.resources.limitPids = 100
	service, err := opt.ToService(context.Background(), &fakeClient{}, flags)
	assert.NilError(t, err)
	assert.Equal(t, service.TaskTemplate.Resources.Limits.Pids, int64(100))
}

func TestToServiceUpdateRollback(t *testing.T) {
	expected := swarm.ServiceSpec{
		UpdateConfig: &swarm.UpdateConfig{
			Parallelism:     23,
			Delay:           34 * time.Second,
			Monitor:         54321 * time.Nanosecond,
			FailureAction:   swarm.UpdateFailureActionPause,
			MaxFailureRatio: 0.6,
			Order:           swarm.UpdateOrderStopFirst,
		},
		RollbackConfig: &swarm.UpdateConfig{
			Parallelism:     12,
			Delay:           23 * time.Second,
			Monitor:         12345 * time.Nanosecond,
			FailureAction:   swarm.UpdateFailureActionContinue,
			MaxFailureRatio: 0.5,
			Order:           swarm.UpdateOrderStartFirst,
		},
	}

	// Note: in test-situation, the flags are only used to detect if an option
	// was set; the actual value itself is read from the serviceOptions below.
	flags := newCreateCommand(nil).Flags()
	flags.Set("update-parallelism", "23")
	flags.Set("update-delay", "34s")
	flags.Set("update-monitor", "54321ns")
	flags.Set("update-failure-action", string(swarm.UpdateFailureActionPause))
	flags.Set("update-max-failure-ratio", "0.6")
	flags.Set("update-order", string(swarm.UpdateOrderStopFirst))

	flags.Set("rollback-parallelism", "12")
	flags.Set("rollback-delay", "23s")
	flags.Set("rollback-monitor", "12345ns")
	flags.Set("rollback-failure-action", string(swarm.UpdateFailureActionContinue))
	flags.Set("rollback-max-failure-ratio", "0.5")
	flags.Set("rollback-order", string(swarm.UpdateOrderStartFirst))

	o := newServiceOptions()
	o.mode = "replicated"
	o.update = updateOptions{
		parallelism:     23,
		delay:           34 * time.Second,
		monitor:         54321 * time.Nanosecond,
		onFailure:       string(swarm.UpdateFailureActionPause),
		maxFailureRatio: 0.6,
		order:           string(swarm.UpdateOrderStopFirst),
	}
	o.rollback = updateOptions{
		parallelism:     12,
		delay:           23 * time.Second,
		monitor:         12345 * time.Nanosecond,
		onFailure:       string(swarm.UpdateFailureActionContinue),
		maxFailureRatio: 0.5,
		order:           string(swarm.UpdateOrderStartFirst),
	}

	service, err := o.ToService(context.Background(), &fakeClient{}, flags)
	assert.NilError(t, err)
	assert.Check(t, is.DeepEqual(service.UpdateConfig, expected.UpdateConfig))
	assert.Check(t, is.DeepEqual(service.RollbackConfig, expected.RollbackConfig))
}

func TestToServiceUpdateRollbackOrder(t *testing.T) {
	flags := newCreateCommand(nil).Flags()
	flags.Set("update-order", string(swarm.UpdateOrderStartFirst))
	flags.Set("rollback-order", string(swarm.UpdateOrderStartFirst))

	o := newServiceOptions()
	o.mode = "replicated"
	o.update = updateOptions{order: string(swarm.UpdateOrderStartFirst)}
	o.rollback = updateOptions{order: string(swarm.UpdateOrderStartFirst)}

	service, err := o.ToService(context.Background(), &fakeClient{}, flags)
	assert.NilError(t, err)
	assert.Check(t, is.Equal(string(service.UpdateConfig.Order), o.update.order))
	assert.Check(t, is.Equal(string(service.RollbackConfig.Order), o.rollback.order))
}

func TestToServiceMaxReplicasGlobalModeConflict(t *testing.T) {
	opt := serviceOptions{
		mode:        "global",
		maxReplicas: 1,
	}
	_, err := opt.ToServiceMode()
	assert.Error(t, err, "replicas-max-per-node can only be used with replicated or replicated-job mode")
}

func TestToServiceSysCtls(t *testing.T) {
	o := newServiceOptions()
	o.mode = "replicated"
	o.sysctls.Set("net.ipv4.ip_forward=1")
	o.sysctls.Set("kernel.shmmax=123456")

	expected := map[string]string{"net.ipv4.ip_forward": "1", "kernel.shmmax": "123456"}
	flags := newCreateCommand(nil).Flags()
	service, err := o.ToService(context.Background(), &fakeClient{}, flags)
	assert.NilError(t, err)
	assert.Check(t, is.DeepEqual(service.TaskTemplate.ContainerSpec.Sysctls, expected))
}
