package e2e

import (
	"context"
	"fmt"
	"path"
	"strconv"
	"testing"

	composecli "github.com/compose-spec/compose-go/v2/cli"
	"github.com/compose-spec/compose-go/v2/types"
	"github.com/docker/docker/api/types/image"
	dockerclient "github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
	"github.com/google/go-containerregistry/pkg/name"
	"github.com/google/go-containerregistry/pkg/v1/remote"
	"github.com/psviderski/uncloud/internal/cli"
	cliInternal "github.com/psviderski/uncloud/internal/cli"
	"github.com/psviderski/uncloud/internal/ucind"
	"github.com/psviderski/uncloud/pkg/api"
	"github.com/psviderski/uncloud/pkg/client/compose"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestComposeBuild(t *testing.T) {
	t.Parallel()

	clusterName := "ucind-test.compose-build"
	ctx := context.Background()

	registryInternalPort := "5000/tcp"
	clusterOpts := ucind.CreateClusterOptions{
		Machines: 1,
		PortMap: nat.PortMap{
			// Forwarding the registry port to the host.
			nat.Port(registryInternalPort): []nat.PortBinding{
				{
					HostIP: "127.0.0.1",
					// Explicitly specify the mapped host port for the registry because if not specified,
					// 'docker push' from Docker Desktop is unable to reach the randomly mapped one for some reason.
					HostPort: "50000",
				},
			},
		},
	}
	c, _ := createTestCluster(t, clusterName, clusterOpts, true)

	machine := c.Machines[0]
	client, err := machine.Connect(ctx)
	require.NoError(t, err)

	// Get the (random) assigned host port for the future registry service
	dockerCli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
	require.NoError(t, err)
	containerInfo, err := dockerCli.ContainerInspect(ctx, machine.ContainerName)
	require.NoError(t, err)
	portMap := containerInfo.NetworkSettings.Ports
	registryHostPortStr := portMap[nat.Port(registryInternalPort)][0].HostPort
	registryHostPort, err := strconv.Atoi(registryHostPortStr)
	require.NoError(t, err)
	assert.True(t, registryHostPort > 0, "Expected host port to be a valid number")

	t.Run("build and push images for a basic project", func(t *testing.T) {
		t.Parallel()

		registryServiceName := "registry"
		t.Cleanup(func() {
			removeServices(t, client, registryServiceName)
		})

		// Start registry
		resp, err := client.RunService(ctx, api.ServiceSpec{
			Name: registryServiceName,
			Mode: api.ServiceModeReplicated,
			Container: api.ContainerSpec{
				Image: "registry:2",
			},
			Ports: []api.PortSpec{
				{
					ContainerPort: 5000,
					PublishedPort: 5000,
					Mode:          api.PortModeHost,
					Protocol:      api.ProtocolTCP,
				},
			},
		})
		require.NoError(t, err)
		assert.NotEmpty(t, resp.ID)
		assert.Equal(t, "registry", resp.Name)

		project, err := compose.LoadProject(
			ctx,
			[]string{"fixtures/compose-build-basic/compose.yaml"},
			composecli.WithEnv([]string{
				fmt.Sprintf("TEST_REGISTRY_PORT=%s", registryHostPortStr),
			}),
		)
		require.NoError(t, err)
		servicesToBuild := cliInternal.GetServicesThatNeedBuild(project)
		serviceImage1 := fmt.Sprintf("127.0.0.1:%d/service-first", registryHostPort)
		serviceImage2 := fmt.Sprintf("127.0.0.1:%d/service-second:version2", registryHostPort)
		t.Cleanup(func() {
			// Remove the images after the test
			removeOptions := image.RemoveOptions{Force: true, PruneChildren: true}
			_, err := dockerCli.ImageRemove(ctx, serviceImage1, removeOptions)
			assert.NoErrorf(t, err, "failed to remove image %s on test cleanup", serviceImage1)
			_, err = dockerCli.ImageRemove(ctx, serviceImage2, removeOptions)
			assert.NoErrorf(t, err, "failed to remove image %s on test cleanup", serviceImage2)
		})

		servicesToBuildExpected := map[string]types.ServiceConfig{
			"service-first": {
				Name: "service-first",
				Build: &types.BuildConfig{
					Context:    path.Join(project.WorkingDir, "service-first-dir"),
					Dockerfile: "Dockerfile",
				},
				Image:       serviceImage1,
				Environment: types.NewMappingWithEquals([]string{}),
				Networks: map[string]*types.ServiceNetworkConfig{
					"default": nil,
				},
			},
			"service-second": {
				Name: "service-second",
				Build: &types.BuildConfig{
					Context:    path.Join(project.WorkingDir, "service-second-dir"),
					Dockerfile: "Dockerfile.alt",
				},
				Image:       serviceImage2,
				Environment: types.NewMappingWithEquals([]string{}),
				Networks: map[string]*types.ServiceNetworkConfig{
					"default": nil,
				},
			},
		}
		assert.Equal(t, servicesToBuildExpected, servicesToBuild)

		// Build and push the images
		buildOpts := cli.BuildOptions{
			Push:    true,
			NoCache: false,
		}
		cli.BuildServices(context.Background(), servicesToBuild, buildOpts)

		// Check the image of the first service
		ref1, err := name.NewRepository(fmt.Sprintf("127.0.0.1:%d/service-first", registryHostPort))
		require.NoError(t, err)
		tags, err := remote.List(ref1)
		require.NoError(t, err)

		assert.Equal(t, tags, []string{"latest"}, "Tags for service service-first do not match")

		// Check the image of the second service
		ref2, err := name.NewRepository(fmt.Sprintf("127.0.0.1:%d/service-second", registryHostPort))
		require.NoError(t, err)
		tags, err = remote.List(ref2)
		require.NoError(t, err)

		assert.Equal(t, tags, []string{"version2"}, "Tags for service service-second do not match")
	})
}
