/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package server

import (
	"context"
	"errors"
	"testing"
	"time"

	containerd "github.com/containerd/containerd/v2/client"
	"github.com/containerd/containerd/v2/core/containers"
	criconfig "github.com/containerd/containerd/v2/internal/cri/config"
	snapshotstore "github.com/containerd/containerd/v2/internal/cri/store/snapshot"
	"github.com/containerd/containerd/v2/pkg/cio"
	"github.com/containerd/typeurl/v2"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/stretchr/testify/assert"
	runtime "k8s.io/cri-api/pkg/apis/runtime/v1"

	containerstore "github.com/containerd/containerd/v2/internal/cri/store/container"
	imagestore "github.com/containerd/containerd/v2/internal/cri/store/image"
)

func getContainerStatusTestData(t *testing.T) (*containerstore.Metadata, containerd.Container, *containerstore.Status,
	*imagestore.Image, *runtime.ContainerStatus) {
	imageID := "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
	testID := "test-id"
	config := &runtime.ContainerConfig{
		Metadata: &runtime.ContainerMetadata{
			Name:    "test-name",
			Attempt: 1,
		},
		Image: &runtime.ImageSpec{Image: "test-image"},
		Mounts: []*runtime.Mount{{
			ContainerPath: "test-container-path",
			HostPath:      "test-host-path",
		}},
		Labels:      map[string]string{"a": "b"},
		Annotations: map[string]string{"c": "d"},
	}

	createdAt := time.Now().UnixNano()

	metadata := &containerstore.Metadata{
		ID:        testID,
		Name:      "test-long-name",
		SandboxID: "test-sandbox-id",
		Config:    config,
		ImageRef:  imageID,
		LogPath:   "test-log-path",
	}
	status := &containerstore.Status{
		Pid:       1234,
		CreatedAt: createdAt,
	}
	image := &imagestore.Image{
		ID: imageID,
		References: []string{
			"gcr.io/library/busybox:latest",
			"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
		},
	}

	container := &fakeSpecOnlyContainer{t: t, spec: &specs.Spec{}}

	expected := &runtime.ContainerStatus{
		Id:          testID,
		Metadata:    config.GetMetadata(),
		State:       runtime.ContainerState_CONTAINER_CREATED,
		CreatedAt:   createdAt,
		Image:       &runtime.ImageSpec{Image: "gcr.io/library/busybox:latest"},
		ImageRef:    "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
		Reason:      completeExitReason,
		Labels:      config.GetLabels(),
		Annotations: config.GetAnnotations(),
		Mounts:      config.GetMounts(),
		LogPath:     "test-log-path",
		User:        &runtime.ContainerUser{},
	}

	return metadata, container, status, image, expected
}

func TestToCRIContainerStatus(t *testing.T) {
	for _, test := range []struct {
		desc           string
		startedAt      int64
		finishedAt     int64
		exitCode       int32
		reason         string
		message        string
		expectedState  runtime.ContainerState
		expectedReason string
	}{
		{
			desc:          "container created",
			expectedState: runtime.ContainerState_CONTAINER_CREATED,
		},
		{
			desc:          "container running",
			startedAt:     time.Now().UnixNano(),
			expectedState: runtime.ContainerState_CONTAINER_RUNNING,
		},
		{
			desc:           "container exited with reason",
			startedAt:      time.Now().UnixNano(),
			finishedAt:     time.Now().UnixNano(),
			exitCode:       1,
			reason:         "test-reason",
			message:        "test-message",
			expectedState:  runtime.ContainerState_CONTAINER_EXITED,
			expectedReason: "test-reason",
		},
		{
			desc:           "container exited with exit code 0 without reason",
			startedAt:      time.Now().UnixNano(),
			finishedAt:     time.Now().UnixNano(),
			exitCode:       0,
			message:        "test-message",
			expectedState:  runtime.ContainerState_CONTAINER_EXITED,
			expectedReason: completeExitReason,
		},
		{
			desc:           "container exited with non-zero exit code without reason",
			startedAt:      time.Now().UnixNano(),
			finishedAt:     time.Now().UnixNano(),
			exitCode:       1,
			message:        "test-message",
			expectedState:  runtime.ContainerState_CONTAINER_EXITED,
			expectedReason: errorExitReason,
		},
	} {
		t.Run(test.desc, func(t *testing.T) {

			metadata, ctnr, status, _, expected := getContainerStatusTestData(t)
			// Update status with test case.
			status.StartedAt = test.startedAt
			status.FinishedAt = test.finishedAt
			status.ExitCode = test.exitCode
			status.Reason = test.reason
			status.Message = test.message
			container, err := containerstore.NewContainer(
				*metadata,
				containerstore.WithFakeStatus(*status),
				containerstore.WithContainer(ctnr),
			)
			assert.NoError(t, err)
			// Set expectation based on test case.
			expected.Reason = test.expectedReason
			expected.StartedAt = test.startedAt
			expected.FinishedAt = test.finishedAt
			expected.ExitCode = test.exitCode
			expected.Message = test.message
			patchExceptedWithState(expected, test.expectedState)
			containerStatus, err := toCRIContainerStatus(context.Background(),
				container,
				expected.Image,
				expected.ImageRef)
			assert.Nil(t, err)
			assert.Equal(t, expected, containerStatus, test.desc)
		})
	}
}

// TODO(mikebrow): add a fake containerd container.Container.Spec client api so we can test verbose is true option
func TestToCRIContainerInfo(t *testing.T) {
	metadata, _, status, _, _ := getContainerStatusTestData(t)
	container, err := containerstore.NewContainer(
		*metadata,
		containerstore.WithFakeStatus(*status),
	)
	assert.NoError(t, err)

	info, err := toCRIContainerInfo(context.Background(),
		container,
		false)
	assert.NoError(t, err)
	assert.Nil(t, info)
}

func TestContainerStatus(t *testing.T) {
	for _, test := range []struct {
		desc          string
		exist         bool
		imageExist    bool
		startedAt     int64
		finishedAt    int64
		reason        string
		expectedState runtime.ContainerState
		expectErr     bool
	}{
		{
			desc:          "container created",
			exist:         true,
			imageExist:    true,
			expectedState: runtime.ContainerState_CONTAINER_CREATED,
		},
		{
			desc:          "container running",
			exist:         true,
			imageExist:    true,
			startedAt:     time.Now().UnixNano(),
			expectedState: runtime.ContainerState_CONTAINER_RUNNING,
		},
		{
			desc:          "container exited",
			exist:         true,
			imageExist:    true,
			startedAt:     time.Now().UnixNano(),
			finishedAt:    time.Now().UnixNano(),
			reason:        "test-reason",
			expectedState: runtime.ContainerState_CONTAINER_EXITED,
		},
		{
			desc:       "container not exist",
			exist:      false,
			imageExist: true,
			expectErr:  true,
		},
		{
			desc:       "image not exist",
			exist:      false,
			imageExist: false,
			expectErr:  true,
		},
	} {
		t.Run(test.desc, func(t *testing.T) {
			c := newTestCRIService()
			metadata, ctnr, status, image, expected := getContainerStatusTestData(t)
			// Update status with test case.
			status.StartedAt = test.startedAt
			status.FinishedAt = test.finishedAt
			status.Reason = test.reason
			container, err := containerstore.NewContainer(
				*metadata,
				containerstore.WithFakeStatus(*status),
				containerstore.WithContainer(ctnr),
			)
			assert.NoError(t, err)
			if test.exist {
				assert.NoError(t, c.containerStore.Add(container))
			}
			if test.imageExist {
				imageStore, err := imagestore.NewFakeStore([]imagestore.Image{*image})
				assert.NoError(t, err)
				c.ImageService = &fakeImageService{imageStore: imageStore}
			}
			resp, err := c.ContainerStatus(context.Background(), &runtime.ContainerStatusRequest{ContainerId: container.ID})
			if test.expectErr {
				assert.Error(t, err)
				assert.Nil(t, resp)
				return
			}
			// Set expectation based on test case.
			expected.StartedAt = test.startedAt
			expected.FinishedAt = test.finishedAt
			expected.Reason = test.reason
			patchExceptedWithState(expected, test.expectedState)
			assert.Equal(t, expected, resp.GetStatus())
		})
	}
}

type fakeImageService struct {
	imageStore *imagestore.Store
}

func (s *fakeImageService) RuntimeSnapshotter(ctx context.Context, ociRuntime criconfig.Runtime) string {
	return ""
}

func (s *fakeImageService) UpdateImage(ctx context.Context, r string) error { return nil }

func (s *fakeImageService) CheckImages(ctx context.Context) error { return nil }

func (s *fakeImageService) GetImage(id string) (imagestore.Image, error) { return s.imageStore.Get(id) }

func (s *fakeImageService) GetSnapshot(key, snapshotter string) (snapshotstore.Snapshot, error) {
	return snapshotstore.Snapshot{}, errors.New("not implemented")
}

func (s *fakeImageService) LocalResolve(refOrID string) (imagestore.Image, error) {
	return imagestore.Image{}, errors.New("not implemented")
}

func (s *fakeImageService) ImageFSPaths() map[string]string { return make(map[string]string) }

func (s *fakeImageService) PullImage(context.Context, string, func(string) (string, string, error), *runtime.PodSandboxConfig, string) (string, error) {
	return "", errors.New("not implemented")
}

func patchExceptedWithState(expected *runtime.ContainerStatus, state runtime.ContainerState) {
	expected.State = state
	switch state {
	case runtime.ContainerState_CONTAINER_CREATED:
		expected.StartedAt, expected.FinishedAt = 0, 0
	case runtime.ContainerState_CONTAINER_RUNNING:
		expected.FinishedAt = 0
	}
}

var _ containerd.Container = &fakeSpecOnlyContainer{}

type fakeSpecOnlyContainer struct {
	t         *testing.T
	spec      *specs.Spec
	errOnSpec error
}

// Spec implements client.Container.
func (c *fakeSpecOnlyContainer) Spec(context.Context) (*specs.Spec, error) {
	if c.errOnSpec != nil {
		return nil, c.errOnSpec
	}
	return c.spec, nil
}

// Checkpoint implements client.Container.
func (c *fakeSpecOnlyContainer) Checkpoint(context.Context, string, ...containerd.CheckpointOpts) (containerd.Image, error) {
	c.t.Error("fakeSpecOnlyContainer.Checkpoint: not implemented")
	return nil, errors.New("not implemented")
}

// Delete implements client.Container.
func (c *fakeSpecOnlyContainer) Delete(context.Context, ...containerd.DeleteOpts) error {
	c.t.Error("fakeSpecOnlyContainer.Delete: not implemented")
	return errors.New("not implemented")
}

// Extensions implements client.Container.
func (c *fakeSpecOnlyContainer) Extensions(context.Context) (map[string]typeurl.Any, error) {
	c.t.Error("fakeSpecOnlyContainer.Extensions: not implemented")
	return nil, errors.New("not implemented")
}

// ID implements client.Container.
func (c *fakeSpecOnlyContainer) ID() string {
	c.t.Error("fakeSpecOnlyContainer.ID: not implemented")
	return "" // not implemented
}

// Image implements client.Container.
func (c *fakeSpecOnlyContainer) Image(context.Context) (containerd.Image, error) {
	c.t.Error("fakeSpecOnlyContainer.Image: not implemented")
	return nil, errors.New("not implemented")
}

// Info implements client.Container.
func (c *fakeSpecOnlyContainer) Info(context.Context, ...containerd.InfoOpts) (containers.Container, error) {
	c.t.Error("fakeSpecOnlyContainer.Info: not implemented")
	return containers.Container{}, errors.New("not implemented")
}

// Labels implements client.Container.
func (c *fakeSpecOnlyContainer) Labels(context.Context) (map[string]string, error) {
	c.t.Error("fakeSpecOnlyContainer.Labels: not implemented")
	return nil, errors.New("not implemented")
}

// NewTask implements client.Container.
func (c *fakeSpecOnlyContainer) NewTask(context.Context, cio.Creator, ...containerd.NewTaskOpts) (containerd.Task, error) {
	c.t.Error("fakeSpecOnlyContainer.NewTask: not implemented")
	return nil, errors.New("not implemented")
}

// SetLabels implements client.Container.
func (c *fakeSpecOnlyContainer) SetLabels(context.Context, map[string]string) (map[string]string, error) {
	c.t.Error("fakeSpecOnlyContainer.SetLabels: not implemented")
	return nil, errors.New("not implemented")
}

// Task implements client.Container.
func (c *fakeSpecOnlyContainer) Task(context.Context, cio.Attach) (containerd.Task, error) {
	c.t.Error("fakeSpecOnlyContainer.Task: not implemented")
	return nil, errors.New("not implemented")
}

// Update implements client.Container.
func (c *fakeSpecOnlyContainer) Update(context.Context, ...containerd.UpdateContainerOpts) error {
	c.t.Error("fakeSpecOnlyContainer.Update: not implemented")
	return errors.New("not implemented")
}

// Restore implements client.Container.
func (c *fakeSpecOnlyContainer) Restore(context.Context, cio.Creator, string) (int, error) {
	c.t.Error("fakeSpecOnlyContainer.Restore: not implemented")
	return -1, errors.New("not implemented")
}
