package container

import (
	"testing"

	"github.com/containrrr/watchtower/pkg/container/mocks"
	"github.com/containrrr/watchtower/pkg/filters"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	cli "github.com/docker/docker/client"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

func TestContainer(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Container Suite")
}

var _ = Describe("the container", func() {
	Describe("the client", func() {
		var docker *cli.Client
		var client Client
		BeforeSuite(func() {
			server := mocks.NewMockAPIServer()
			docker, _ = cli.NewClientWithOpts(
				cli.WithHost(server.URL),
				cli.WithHTTPClient(server.Client()))
			client = dockerClient{
				api:        docker,
				pullImages: false,
			}
		})
		It("should return a client for the api", func() {
			Expect(client).NotTo(BeNil())
		})
		When("listing containers without any filter", func() {
			It("should return all available containers", func() {
				containers, err := client.ListContainers(filters.NoFilter)
				Expect(err).NotTo(HaveOccurred())
				Expect(len(containers) == 2).To(BeTrue())
			})
		})
		When("listing containers with a filter matching nothing", func() {
			It("should return an empty array", func() {
				filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter)
				containers, err := client.ListContainers(filter)
				Expect(err).NotTo(HaveOccurred())
				Expect(len(containers) == 0).To(BeTrue())
			})
		})
		When("listing containers with a watchtower filter", func() {
			It("should return only the watchtower container", func() {
				containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
				Expect(err).NotTo(HaveOccurred())
				Expect(len(containers) == 1).To(BeTrue())
				Expect(containers[0].ImageName()).To(Equal("containrrr/watchtower:latest"))
			})
		})
		When(`listing containers with the "include stopped" option`, func() {
			It("should return both stopped and running containers", func() {
				client = dockerClient{
					api:            docker,
					pullImages:     false,
					includeStopped: true,
				}
				containers, err := client.ListContainers(filters.NoFilter)
				Expect(err).NotTo(HaveOccurred())
				Expect(len(containers) > 0).To(BeTrue())
			})
		})
		When(`listing containers with the "include restart" option`, func() {
			It("should return both stopped, restarting and running containers", func() {
				client = dockerClient{
					api:               docker,
					pullImages:        false,
					includeRestarting: true,
				}
				containers, err := client.ListContainers(filters.NoFilter)
				Expect(err).NotTo(HaveOccurred())
				RestartingContainerFound := false
				for _, ContainerRunning := range containers {
					if ContainerRunning.containerInfo.State.Restarting {
						RestartingContainerFound = true
					}
				}
				Expect(RestartingContainerFound).To(BeTrue())
				Expect(RestartingContainerFound).NotTo(BeFalse())
			})
		})
		When(`listing containers without restarting ones`, func() {
			It("should not return restarting containers", func() {
				client = dockerClient{
					api:               docker,
					pullImages:        false,
					includeRestarting: false,
				}
				containers, err := client.ListContainers(filters.NoFilter)
				Expect(err).NotTo(HaveOccurred())
				RestartingContainerFound := false
				for _, ContainerRunning := range containers {
					if ContainerRunning.containerInfo.State.Restarting {
						RestartingContainerFound = true
					}
				}
				Expect(RestartingContainerFound).To(BeFalse())
				Expect(RestartingContainerFound).NotTo(BeTrue())
			})
		})
	})
	When("asked for metadata", func() {
		var c *Container
		BeforeEach(func() {
			c = mockContainerWithLabels(map[string]string{
				"com.centurylinklabs.watchtower.enable": "true",
				"com.centurylinklabs.watchtower":        "true",
			})
		})
		It("should return its name on calls to .Name()", func() {
			name := c.Name()
			Expect(name).To(Equal("test-containrrr"))
			Expect(name).NotTo(Equal("wrong-name"))
		})
		It("should return its ID on calls to .ID()", func() {
			id := c.ID()

			Expect(id).To(Equal("container_id"))
			Expect(id).NotTo(Equal("wrong-id"))
		})
		It("should return true, true if enabled on calls to .Enabled()", func() {
			enabled, exists := c.Enabled()

			Expect(enabled).To(BeTrue())
			Expect(enabled).NotTo(BeFalse())
			Expect(exists).To(BeTrue())
			Expect(exists).NotTo(BeFalse())
		})
		It("should return false, true if present but not true on calls to .Enabled()", func() {
			c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "false"})
			enabled, exists := c.Enabled()

			Expect(enabled).To(BeFalse())
			Expect(enabled).NotTo(BeTrue())
			Expect(exists).To(BeTrue())
			Expect(exists).NotTo(BeFalse())
		})
		It("should return false, false if not present on calls to .Enabled()", func() {
			c = mockContainerWithLabels(map[string]string{"lol": "false"})
			enabled, exists := c.Enabled()

			Expect(enabled).To(BeFalse())
			Expect(enabled).NotTo(BeTrue())
			Expect(exists).To(BeFalse())
			Expect(exists).NotTo(BeTrue())
		})
		It("should return false, false if present but not parsable .Enabled()", func() {
			c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "falsy"})
			enabled, exists := c.Enabled()

			Expect(enabled).To(BeFalse())
			Expect(enabled).NotTo(BeTrue())
			Expect(exists).To(BeFalse())
			Expect(exists).NotTo(BeTrue())
		})
		When("checking if its a watchtower instance", func() {
			It("should return true if the label is set to true", func() {
				isWatchtower := c.IsWatchtower()
				Expect(isWatchtower).To(BeTrue())
			})
			It("should return false if the label is present but set to false", func() {
				c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower": "false"})
				isWatchtower := c.IsWatchtower()
				Expect(isWatchtower).To(BeFalse())
			})
			It("should return false if the label is not present", func() {
				c = mockContainerWithLabels(map[string]string{"funny.label": "false"})
				isWatchtower := c.IsWatchtower()
				Expect(isWatchtower).To(BeFalse())
			})
			It("should return false if there are no labels", func() {
				c = mockContainerWithLabels(map[string]string{})
				isWatchtower := c.IsWatchtower()
				Expect(isWatchtower).To(BeFalse())
			})
		})
		When("fetching the custom stop signal", func() {
			It("should return the signal if its set", func() {
				c = mockContainerWithLabels(map[string]string{
					"com.centurylinklabs.watchtower.stop-signal": "SIGKILL",
				})
				stopSignal := c.StopSignal()
				Expect(stopSignal).To(Equal("SIGKILL"))
			})
			It("should return an empty string if its not set", func() {
				c = mockContainerWithLabels(map[string]string{})
				stopSignal := c.StopSignal()
				Expect(stopSignal).To(Equal(""))
			})
		})
		When("fetching the image name", func() {
			When("the zodiac label is present", func() {
				It("should fetch the image name from it", func() {
					c = mockContainerWithLabels(map[string]string{
						"com.centurylinklabs.zodiac.original-image": "the-original-image",
					})
					imageName := c.ImageName()
					Expect(imageName).To(Equal(imageName))
				})
			})
			It("should return the image name", func() {
				name := "image-name:3"
				c = mockContainerWithImageName(name)
				imageName := c.ImageName()
				Expect(imageName).To(Equal(name))
			})
			It("should assume latest if no tag is supplied", func() {
				name := "image-name"
				c = mockContainerWithImageName(name)
				imageName := c.ImageName()
				Expect(imageName).To(Equal(name + ":latest"))
			})
		})

		When("fetching container links", func() {
			When("the depends on label is present", func() {
				It("should fetch depending containers from it", func() {
					c = mockContainerWithLabels(map[string]string{
						"com.centurylinklabs.watchtower.depends-on": "postgres",
					})
					links := c.Links()
					Expect(links).To(SatisfyAll(ContainElement("postgres"), HaveLen(1)))
				})
				It("should fetch depending containers if there are many", func() {
					c = mockContainerWithLabels(map[string]string{
						"com.centurylinklabs.watchtower.depends-on": "postgres,redis",
					})
					links := c.Links()
					Expect(links).To(SatisfyAll(ContainElement("postgres"), ContainElement("redis"), HaveLen(2)))
				})
				It("should fetch depending containers if label is blank", func() {
					c = mockContainerWithLabels(map[string]string{
						"com.centurylinklabs.watchtower.depends-on": "",
					})
					links := c.Links()
					Expect(links).To(HaveLen(0))
				})
			})
			When("the depends on label is not present", func() {
				It("should fetch depending containers from host config links", func() {
					c = mockContainerWithLinks([]string{
						"redis:test-containrrr",
						"postgres:test-containrrr",
					})
					links := c.Links()
					Expect(links).To(SatisfyAll(ContainElement("redis"), ContainElement("postgres"), HaveLen(2)))
				})
			})
		})
	})
})

func mockContainerWithImageName(name string) *Container {
	container := mockContainerWithLabels(nil)
	container.containerInfo.Config.Image = name
	return container
}

func mockContainerWithLinks(links []string) *Container {
	content := types.ContainerJSON{
		ContainerJSONBase: &types.ContainerJSONBase{
			ID:    "container_id",
			Image: "image",
			Name:  "test-containrrr",
			HostConfig: &container.HostConfig{
				Links: links,
			},
		},
		Config: &container.Config{
			Labels: map[string]string{},
		},
	}
	return NewContainer(&content, nil)
}

func mockContainerWithLabels(labels map[string]string) *Container {
	content := types.ContainerJSON{
		ContainerJSONBase: &types.ContainerJSONBase{
			ID:    "container_id",
			Image: "image",
			Name:  "test-containrrr",
		},
		Config: &container.Config{
			Labels: labels,
		},
	}
	return NewContainer(&content, nil)
}