package service

import (
	"context"
	"io"
	"io/ioutil"
	"os"
	"path"
	"testing"

	"github.com/docker/docker/api/types"
	swarmtypes "github.com/docker/docker/api/types/swarm"
	"github.com/docker/docker/api/types/swarm/runtime"
	"github.com/docker/docker/integration/internal/swarm"
	"github.com/docker/docker/internal/test/daemon"
	"github.com/docker/docker/internal/test/fixtures/plugin"
	"github.com/docker/docker/internal/test/registry"
	"gotest.tools/assert"
	"gotest.tools/poll"
	"gotest.tools/skip"
)

func TestServicePlugin(t *testing.T) {
	skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
	skip.If(t, os.Getenv("DOCKER_ENGINE_GOARCH") != "amd64")
	defer setupTest(t)()

	reg := registry.NewV2(t)
	defer reg.Close()

	repo := path.Join(registry.DefaultURL, "swarm", "test:v1")
	repo2 := path.Join(registry.DefaultURL, "swarm", "test:v2")
	name := "test"

	d := daemon.New(t)
	d.StartWithBusybox(t)
	apiclient := d.NewClientT(t)
	err := plugin.Create(context.Background(), apiclient, repo)
	assert.NilError(t, err)
	r, err := apiclient.PluginPush(context.Background(), repo, "")
	assert.NilError(t, err)
	_, err = io.Copy(ioutil.Discard, r)
	assert.NilError(t, err)
	err = apiclient.PluginRemove(context.Background(), repo, types.PluginRemoveOptions{})
	assert.NilError(t, err)
	err = plugin.Create(context.Background(), apiclient, repo2)
	assert.NilError(t, err)
	r, err = apiclient.PluginPush(context.Background(), repo2, "")
	assert.NilError(t, err)
	_, err = io.Copy(ioutil.Discard, r)
	assert.NilError(t, err)
	err = apiclient.PluginRemove(context.Background(), repo2, types.PluginRemoveOptions{})
	assert.NilError(t, err)
	d.Stop(t)

	d1 := swarm.NewSwarm(t, testEnv, daemon.WithExperimental)
	defer d1.Stop(t)
	d2 := daemon.New(t, daemon.WithExperimental, daemon.WithSwarmPort(daemon.DefaultSwarmPort+1))
	d2.StartAndSwarmJoin(t, d1, true)
	defer d2.Stop(t)
	d3 := daemon.New(t, daemon.WithExperimental, daemon.WithSwarmPort(daemon.DefaultSwarmPort+2))
	d3.StartAndSwarmJoin(t, d1, false)
	defer d3.Stop(t)

	id := d1.CreateService(t, makePlugin(repo, name, nil))
	poll.WaitOn(t, d1.PluginIsRunning(name), swarm.ServicePoll)
	poll.WaitOn(t, d2.PluginIsRunning(name), swarm.ServicePoll)
	poll.WaitOn(t, d3.PluginIsRunning(name), swarm.ServicePoll)

	service := d1.GetService(t, id)
	d1.UpdateService(t, service, makePlugin(repo2, name, nil))
	poll.WaitOn(t, d1.PluginReferenceIs(name, repo2), swarm.ServicePoll)
	poll.WaitOn(t, d2.PluginReferenceIs(name, repo2), swarm.ServicePoll)
	poll.WaitOn(t, d3.PluginReferenceIs(name, repo2), swarm.ServicePoll)
	poll.WaitOn(t, d1.PluginIsRunning(name), swarm.ServicePoll)
	poll.WaitOn(t, d2.PluginIsRunning(name), swarm.ServicePoll)
	poll.WaitOn(t, d3.PluginIsRunning(name), swarm.ServicePoll)

	d1.RemoveService(t, id)
	poll.WaitOn(t, d1.PluginIsNotPresent(name), swarm.ServicePoll)
	poll.WaitOn(t, d2.PluginIsNotPresent(name), swarm.ServicePoll)
	poll.WaitOn(t, d3.PluginIsNotPresent(name), swarm.ServicePoll)

	// constrain to managers only
	id = d1.CreateService(t, makePlugin(repo, name, []string{"node.role==manager"}))
	poll.WaitOn(t, d1.PluginIsRunning(name), swarm.ServicePoll)
	poll.WaitOn(t, d2.PluginIsRunning(name), swarm.ServicePoll)
	poll.WaitOn(t, d3.PluginIsNotPresent(name), swarm.ServicePoll)

	d1.RemoveService(t, id)
	poll.WaitOn(t, d1.PluginIsNotPresent(name), swarm.ServicePoll)
	poll.WaitOn(t, d2.PluginIsNotPresent(name), swarm.ServicePoll)
	poll.WaitOn(t, d3.PluginIsNotPresent(name), swarm.ServicePoll)

	// with no name
	id = d1.CreateService(t, makePlugin(repo, "", nil))
	poll.WaitOn(t, d1.PluginIsRunning(repo), swarm.ServicePoll)
	poll.WaitOn(t, d2.PluginIsRunning(repo), swarm.ServicePoll)
	poll.WaitOn(t, d3.PluginIsRunning(repo), swarm.ServicePoll)

	d1.RemoveService(t, id)
	poll.WaitOn(t, d1.PluginIsNotPresent(repo), swarm.ServicePoll)
	poll.WaitOn(t, d2.PluginIsNotPresent(repo), swarm.ServicePoll)
	poll.WaitOn(t, d3.PluginIsNotPresent(repo), swarm.ServicePoll)
}

func makePlugin(repo, name string, constraints []string) func(*swarmtypes.Service) {
	return func(s *swarmtypes.Service) {
		s.Spec.TaskTemplate.Runtime = swarmtypes.RuntimePlugin
		s.Spec.TaskTemplate.PluginSpec = &runtime.PluginSpec{
			Name:   name,
			Remote: repo,
		}
		if constraints != nil {
			s.Spec.TaskTemplate.Placement = &swarmtypes.Placement{
				Constraints: constraints,
			}
		}
	}
}
