package swarm

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/swarm"
	"github.com/docker/docker/client"
	"github.com/portainer/portainer/api/filesystem"
	"github.com/portainer/portainer/pkg/libstack"
	"github.com/stretchr/testify/require"
)

// ensureSwarmMode ensures the Docker daemon is a swarm manager for the duration
// of the test. If the daemon is inactive, it initialises a single-node swarm and
// registers a cleanup to leave it afterwards. If the daemon is already a manager
// it does nothing. If the daemon is a worker it fails the test immediately.
func ensureSwarmMode(t *testing.T) *client.Client {
	t.Helper()

	apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, apiClient.Close()) })

	info, err := apiClient.Info(t.Context())
	require.NoError(t, err)

	switch info.Swarm.LocalNodeState {
	case swarm.LocalNodeStateInactive:
		_, err = apiClient.SwarmInit(t.Context(), swarm.InitRequest{
			ListenAddr:    "0.0.0.0:2377",
			AdvertiseAddr: "127.0.0.1",
		})
		require.NoError(t, err)
		t.Cleanup(func() { require.NoError(t, apiClient.SwarmLeave(context.Background(), true)) })
	case swarm.LocalNodeStateActive:
		if !info.Swarm.ControlAvailable {
			t.Fatal("docker daemon is a swarm worker, not a manager: cannot run swarm stack tests")
		}
		// already a manager - don't tear down, don't disrupt
	default:
		t.Fatalf("unexpected swarm node state: %s", info.Swarm.LocalNodeState)
	}

	return apiClient
}

// serviceExists reports whether a service named <stackName>_<serviceName> exists.
func serviceExists(t *testing.T, apiClient client.APIClient, stackName, serviceName string) bool {
	fullName := stackName + "_" + serviceName

	services, err := apiClient.ServiceList(t.Context(), swarm.ServiceListOptions{
		Filters: filters.NewArgs(filters.KeyValuePair{Key: "name", Value: fullName}),
	})
	require.NoError(t, err)

	for _, svc := range services {
		if svc.Spec.Name == fullName {
			return true
		}
	}

	return false
}

func createComposeFile(t *testing.T, dir, name, content string) string {
	t.Helper()

	path := filesystem.JoinPaths(dir, name)
	require.NoError(t, os.WriteFile(path, []byte(content), 0o644))

	return path
}

func TestSwarmDeployWithRemoveOrphans(t *testing.T) {
	ensureIntegrationTest(t)

	apiClient := ensureSwarmMode(t)

	const projectName = "swarm_orphan_test"

	const twoServiceContent = `version: '3'
services:
  service-1:
    image: alpine:latest
    command: ["sh", "-c", "while true; do sleep 3600; done"]
  service-2:
    image: alpine:latest
    command: ["sh", "-c", "while true; do sleep 3600; done"]`

	const oneServiceContent = `version: '3'
services:
  service-2:
    image: alpine:latest
    command: ["sh", "-c", "while true; do sleep 3600; done"]`

	deployer := NewSwarmDeployer()
	dir := t.TempDir()

	twoServicePath := createComposeFile(t, dir, "two-services.yml", twoServiceContent)
	oneServicePath := createComposeFile(t, dir, "one-service.yml", oneServiceContent)

	err := deployer.Deploy(
		t.Context(),
		[]string{twoServicePath},
		DeployOptions{Options: Options{ProjectName: projectName}},
	)
	require.NoError(t, err)

	t.Cleanup(func() {
		err := deployer.Remove(context.Background(), projectName, RemoveOptions{})
		require.NoError(t, err)
	})

	ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
	t.Cleanup(func() { cancel() })

	result := deployer.WaitForStatus(ctx, projectName, Options{}, libstack.StatusRunning)
	require.Empty(t, result.ErrorMsg)
	require.Equal(t, libstack.StatusRunning, result.Status)

	require.True(t, serviceExists(t, apiClient, projectName, "service-1"))
	require.True(t, serviceExists(t, apiClient, projectName, "service-2"))

	err = deployer.Deploy(ctx, []string{oneServicePath}, DeployOptions{
		Options:       Options{ProjectName: projectName},
		RemoveOrphans: true,
	})
	require.NoError(t, err)

	result = deployer.WaitForStatus(ctx, projectName, Options{}, libstack.StatusRunning)
	require.Empty(t, result.ErrorMsg)
	require.Equal(t, libstack.StatusRunning, result.Status)

	require.False(t, serviceExists(t, apiClient, projectName, "service-1"))
	require.True(t, serviceExists(t, apiClient, projectName, "service-2"))
}

func TestSwarmDeployWithEnvVars(t *testing.T) {
	ensureIntegrationTest(t)
	ensureSwarmMode(t)

	const projectName = "swarm_envvar_test"

	const composeContent = `version: '3'
services:
  web:
    image: alpine:${TAG}
    command: ["sh", "-c", "while true; do sleep 3600; done"]`

	deployer := NewSwarmDeployer()
	dir := t.TempDir()

	path := createComposeFile(t, dir, "envvar.yml", composeContent)

	err := deployer.Deploy(t.Context(), []string{path}, DeployOptions{
		Options: Options{
			ProjectName: projectName,
			Env:         []string{"TAG=latest"},
		},
	})
	require.NoError(t, err)

	t.Cleanup(func() {
		err := deployer.Remove(context.Background(), projectName, RemoveOptions{})
		require.NoError(t, err)
	})

	ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
	t.Cleanup(func() { cancel() })

	result := deployer.WaitForStatus(ctx, projectName, Options{}, libstack.StatusRunning)
	require.Empty(t, result.ErrorMsg)
	require.Equal(t, libstack.StatusRunning, result.Status)
}
