package network_test

import (
	"context"
	"net/netip"
	"os"
	"testing"
	"time"

	"github.com/moby/moby/api/types/container"
	dockernetwork "github.com/moby/moby/api/types/network"
	"github.com/moby/moby/client"
	"github.com/stretchr/testify/require"

	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/internal/core"
	"github.com/testcontainers/testcontainers-go/network"
	"github.com/testcontainers/testcontainers-go/wait"
)

const (
	nginxAlpineImage = "nginx:alpine"
	nginxDefaultPort = "80/tcp"
)

func TestNew(t *testing.T) {
	ctx := context.Background()

	net, err := network.New(ctx,
		network.WithAttachable(),
		network.WithDriver("bridge"),
		network.WithLabels(map[string]string{"this-is-a-test": "value"}),
	)
	require.NoError(t, err)
	defer func() {
		require.NoError(t, net.Remove(ctx))
	}()

	networkName := net.Name

	nginxC, err := testcontainers.Run(ctx, nginxAlpineImage,
		testcontainers.WithExposedPorts(nginxDefaultPort),
		network.WithNetwork([]string{"nginx"}, net),
	)
	testcontainers.CleanupContainer(t, nginxC)
	require.NoError(t, err)

	apiClient, err := testcontainers.NewDockerClientWithOpts(context.Background())
	require.NoError(t, err)

	resources, err := apiClient.NetworkList(context.Background(), client.NetworkListOptions{
		Filters: make(client.Filters).Add("name", networkName),
	})
	require.NoError(t, err)
	require.Len(t, resources.Items, 1)

	newNetwork := resources.Items[0]
	expectedLabels := testcontainers.GenericLabels()
	expectedLabels["this-is-a-test"] = "true"

	require.True(t, newNetwork.Attachable)
	require.False(t, newNetwork.Internal)
	require.Equal(t, "value", newNetwork.Labels["this-is-a-test"])
	require.NoError(t, testcontainers.TerminateContainer(nginxC))
}

// testNetworkAliases {
func TestContainerAttachedToNewNetwork(t *testing.T) {
	ctx := context.Background()

	newNetwork, err := network.New(ctx)
	require.NoError(t, err)
	testcontainers.CleanupNetwork(t, newNetwork)

	networkName := newNetwork.Name

	aliases := []string{"alias1", "alias2", "alias3"}

	nginx, err := testcontainers.Run(ctx, nginxAlpineImage,
		testcontainers.WithExposedPorts(nginxDefaultPort),
		network.WithNetwork(aliases, newNetwork),
	)
	testcontainers.CleanupContainer(t, nginx)
	require.NoError(t, err)

	networks, err := nginx.Networks(ctx)
	require.NoError(t, err)
	require.Len(t, networks, 1)

	nw := networks[0]
	require.Equal(t, networkName, nw)

	networkAliases, err := nginx.NetworkAliases(ctx)
	require.NoError(t, err)

	require.Len(t, networkAliases, 1)
	networkAlias := networkAliases[networkName]
	require.NotEmpty(t, networkAlias)

	for _, alias := range aliases {
		require.Contains(t, networkAlias, alias)
	}

	networkIP, err := nginx.ContainerIP(ctx)
	require.NoError(t, err)
	require.NotEmpty(t, networkIP)
}

// }

func TestContainerIPs(t *testing.T) {
	ctx := context.Background()

	newNetwork, err := network.New(ctx)
	require.NoError(t, err)
	testcontainers.CleanupNetwork(t, newNetwork)

	nginx, err := testcontainers.Run(ctx, nginxAlpineImage,
		testcontainers.WithExposedPorts(nginxDefaultPort),
		network.WithNetwork([]string{"nginx"}, newNetwork),
		network.WithBridgeNetwork(),
		testcontainers.WithWaitStrategy(wait.ForListeningPort(nginxDefaultPort)),
	)
	testcontainers.CleanupContainer(t, nginx)
	require.NoError(t, err)

	ips, err := nginx.ContainerIPs(ctx)
	require.NoError(t, err)
	require.Len(t, ips, 2)
}

func TestContainerWithReaperNetwork(t *testing.T) {
	if core.IsWindows() {
		t.Skip("Skip for Windows. See https://stackoverflow.com/questions/43784916/docker-for-windows-networking-container-with-multiple-network-interfaces")
	}

	ctx := context.Background()
	maxNetworksCount := 2
	networks := make([]string, 0, maxNetworksCount)

	opts := make([]testcontainers.ContainerCustomizer, 0, 2+maxNetworksCount)
	opts = append(opts,
		testcontainers.WithExposedPorts(nginxDefaultPort),
		testcontainers.WithWaitStrategy(
			wait.ForListeningPort(nginxDefaultPort),
			wait.ForLog("Configuration complete; ready for start up"),
		),
	)

	for range maxNetworksCount {
		n, err := network.New(ctx)
		require.NoError(t, err)
		testcontainers.CleanupNetwork(t, n)

		networks = append(networks, n.Name)
		opts = append(opts, network.WithNetwork([]string{"nginx"}, n))
	}

	nginx, err := testcontainers.Run(ctx, nginxAlpineImage, opts...)
	testcontainers.CleanupContainer(t, nginx)
	require.NoError(t, err)

	containerID := nginx.GetContainerID()

	cli, err := testcontainers.NewDockerClientWithOpts(ctx)
	require.NoError(t, err)
	defer cli.Close()

	res, err := cli.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{})
	require.NoError(t, err)
	cnt := res.Container
	require.Len(t, cnt.NetworkSettings.Networks, maxNetworksCount)
	require.NotNil(t, cnt.NetworkSettings.Networks[networks[0]])
	require.NotNil(t, cnt.NetworkSettings.Networks[networks[1]])
}

func TestMultipleContainersInTheNewNetwork(t *testing.T) {
	ctx := context.Background()

	net, err := network.New(ctx, network.WithDriver("bridge"))
	require.NoError(t, err)
	testcontainers.CleanupNetwork(t, net)

	networkName := net.Name

	c1, err := testcontainers.Run(ctx, nginxAlpineImage,
		network.WithNetwork([]string{"nginx1"}, net),
	)
	testcontainers.CleanupContainer(t, c1)
	require.NoError(t, err)

	c2, err := testcontainers.Run(ctx, nginxAlpineImage,
		network.WithNetwork([]string{"nginx2"}, net),
	)
	testcontainers.CleanupContainer(t, c2)
	require.NoError(t, err)

	pNets, err := c1.Networks(ctx)
	require.NoError(t, err)

	rNets, err := c2.Networks(ctx)
	require.NoError(t, err)

	require.Len(t, pNets, 1)
	require.Len(t, rNets, 1)

	require.Equal(t, networkName, pNets[0])
	require.Equal(t, networkName, rNets[0])
}

func TestNew_withOptions(t *testing.T) {
	// newNetworkWithOptions {
	ctx := context.Background()

	// dockernetwork is the alias used for github.com/docker/docker/api/types/network
	ipamConfig := dockernetwork.IPAM{
		Driver: "default",
		Config: []dockernetwork.IPAMConfig{
			{
				Subnet:  netip.MustParsePrefix("10.1.1.0/24"),
				Gateway: netip.MustParseAddr("10.1.1.254"),
			},
		},
		Options: map[string]string{
			"driver": "host-local",
		},
	}
	net, err := network.New(ctx,
		network.WithIPAM(&ipamConfig),
		network.WithAttachable(),
		network.WithDriver("bridge"),
	)
	// }
	require.NoError(t, err)
	testcontainers.CleanupNetwork(t, net)

	networkName := net.Name

	nginx, err := testcontainers.Run(ctx, nginxAlpineImage,
		network.WithNetwork([]string{"nginx"}, net),
		testcontainers.WithExposedPorts(nginxDefaultPort),
	)
	testcontainers.CleanupContainer(t, nginx)
	require.NoError(t, err)

	provider, err := testcontainers.ProviderDocker.GetProvider()
	require.NoError(t, err)
	defer provider.Close()

	//nolint:staticcheck
	foundNetwork, err := provider.GetNetwork(ctx, testcontainers.NetworkRequest{Name: networkName})
	require.NoError(t, err)
	require.Equal(t, ipamConfig, foundNetwork.IPAM)
}

func TestWithNetwork(t *testing.T) {
	// first create the network to be reused
	nw, err := network.New(context.Background(), network.WithLabels(map[string]string{"network-type": "unique"}))
	require.NoError(t, err)
	testcontainers.CleanupNetwork(t, nw)

	networkName := nw.Name

	// check that the network is reused, multiple times
	for range 100 {
		req := testcontainers.GenericContainerRequest{
			ContainerRequest: testcontainers.ContainerRequest{},
		}

		err := network.WithNetwork([]string{"alias"}, nw)(&req)
		require.NoError(t, err)

		require.Len(t, req.Networks, 1)
		require.Equal(t, networkName, req.Networks[0])

		require.Len(t, req.NetworkAliases, 1)
		require.Equal(t, map[string][]string{networkName: {"alias"}}, req.NetworkAliases)
	}

	// verify that the network is created only once
	apiClient, err := testcontainers.NewDockerClientWithOpts(context.Background())
	require.NoError(t, err)

	resources, err := apiClient.NetworkList(context.Background(), client.NetworkListOptions{
		Filters: make(client.Filters).Add("name", networkName),
	})
	require.NoError(t, err)
	require.Len(t, resources.Items, 1)

	newNetwork := resources.Items[0]

	expectedLabels := testcontainers.GenericLabels()
	expectedLabels["network-type"] = "unique"

	require.Equal(t, networkName, newNetwork.Name)
	require.False(t, newNetwork.Attachable)
	require.False(t, newNetwork.Internal)
	require.Equal(t, expectedLabels, newNetwork.Labels)
}

func TestWithNetworkName(t *testing.T) {
	t.Run("bridge/success", func(t *testing.T) {
		req := testcontainers.GenericContainerRequest{
			ContainerRequest: testcontainers.ContainerRequest{},
		}

		err := network.WithBridgeNetwork()(&req)
		require.NoError(t, err)

		require.Len(t, req.Networks, 1)
		require.Equal(t, "bridge", req.Networks[0])
	})

	t.Run("bridge/error/network-scoped-alias", func(t *testing.T) {
		req := testcontainers.GenericContainerRequest{
			ContainerRequest: testcontainers.ContainerRequest{},
		}

		err := network.WithNetworkName([]string{"alias"}, "bridge")(&req)
		require.Error(t, err)
	})

	t.Run("user-defined/success", func(t *testing.T) {
		req := testcontainers.GenericContainerRequest{
			ContainerRequest: testcontainers.ContainerRequest{},
		}

		err := network.WithNetworkName([]string{"alias"}, "user-defined")(&req)
		require.NoError(t, err)

		require.Len(t, req.Networks, 1)
		require.Equal(t, "user-defined", req.Networks[0])

		require.Len(t, req.NetworkAliases, 1)
		require.Equal(t, map[string][]string{"user-defined": {"alias"}}, req.NetworkAliases)
	})
}

func TestWithSyntheticNetwork(t *testing.T) {
	nw := &testcontainers.DockerNetwork{
		Name: "synthetic-network",
	}

	networkName := nw.Name

	req := testcontainers.GenericContainerRequest{
		ContainerRequest: testcontainers.ContainerRequest{
			Image: nginxAlpineImage,
		},
	}

	err := network.WithNetwork([]string{"alias"}, nw)(&req)
	require.NoError(t, err)

	require.Len(t, req.Networks, 1)
	require.Equal(t, networkName, req.Networks[0])

	require.Len(t, req.NetworkAliases, 1)
	require.Equal(t, map[string][]string{networkName: {"alias"}}, req.NetworkAliases)

	// verify that the network is created only once
	apiClient, err := testcontainers.NewDockerClientWithOpts(context.Background())
	require.NoError(t, err)

	resources, err := apiClient.NetworkList(context.Background(), client.NetworkListOptions{
		Filters: make(client.Filters).Add("name", networkName),
	})
	require.NoError(t, err)
	require.Empty(t, resources.Items) // no Docker network was created
}

func TestWithNewNetwork(t *testing.T) {
	req := testcontainers.GenericContainerRequest{
		ContainerRequest: testcontainers.ContainerRequest{},
	}

	err := network.WithNewNetwork(context.Background(), []string{"alias"},
		network.WithAttachable(),
		network.WithInternal(),
		network.WithLabels(map[string]string{"this-is-a-test": "value"}),
	)(&req)
	require.NoError(t, err)
	require.Len(t, req.Networks, 1)

	networkName := req.Networks[0]

	require.Len(t, req.NetworkAliases, 1)
	require.Equal(t, map[string][]string{networkName: {"alias"}}, req.NetworkAliases)

	apiClient, err := testcontainers.NewDockerClientWithOpts(context.Background())
	require.NoError(t, err)

	resources, err := apiClient.NetworkList(context.Background(), client.NetworkListOptions{
		Filters: make(client.Filters).Add("name", networkName),
	})
	require.NoError(t, err)
	require.Len(t, resources.Items, 1)

	newNetwork := resources.Items[0]
	defer func() {
		_, err := apiClient.NetworkRemove(context.Background(), newNetwork.ID, client.NetworkRemoveOptions{})
		require.NoError(t, err)
	}()

	expectedLabels := testcontainers.GenericLabels()
	expectedLabels["this-is-a-test"] = "value"

	require.Equal(t, networkName, newNetwork.Name)
	require.True(t, newNetwork.Attachable)
	require.True(t, newNetwork.Internal)
	require.Equal(t, expectedLabels, newNetwork.Labels)
}

func TestWithNewNetworkContextTimeout(t *testing.T) {
	req := testcontainers.GenericContainerRequest{
		ContainerRequest: testcontainers.ContainerRequest{},
	}

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
	defer cancel()

	err := network.WithNewNetwork(ctx, []string{"alias"},
		network.WithAttachable(),
		network.WithInternal(),
		network.WithLabels(map[string]string{"this-is-a-test": "value"}),
	)(&req)
	require.Error(t, err)

	// we do not want to fail, just skip the network creation
	require.Empty(t, req.Networks)
	require.Empty(t, req.NetworkAliases)
}

func TestCleanupWithNil(t *testing.T) {
	var nw *testcontainers.DockerNetwork
	testcontainers.CleanupNetwork(t, nw)
}

func TestContainerWithNetworkModeAndNetworkTogether(t *testing.T) {
	if os.Getenv("XDG_RUNTIME_DIR") != "" {
		t.Skip("Skipping test that requires host network access when running in a container")
	}

	// skipIfDockerDesktop {
	ctx := context.Background()
	testcontainers.SkipIfDockerDesktop(t, ctx)
	// }

	nginx, err := testcontainers.Run(
		ctx, nginxAlpineImage,
		testcontainers.WithHostConfigModifier(func(hc *container.HostConfig) {
			hc.NetworkMode = "host"
		}),
		network.WithNetworkName([]string{"nginx"}, "new-network"),
	)
	testcontainers.CleanupContainer(t, nginx)
	if err != nil {
		// Error when NetworkMode = host and Network = []string{"bridge"}
		t.Logf("Can't use Network and NetworkMode together, %s\n", err)
	}
}
