// Copyright 2023 Northern.tech AS
//
//    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 app

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/mendersoftware/mender-artifact/areader"
	"github.com/mendersoftware/mender-artifact/artifact"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"

	"github.com/mendersoftware/mender-server/pkg/api/client"
	oas_client "github.com/mendersoftware/mender-server/pkg/api/client/mocks"
	"github.com/mendersoftware/mender-server/pkg/identity"
	"github.com/mendersoftware/mender-server/pkg/utils/types"

	"github.com/mendersoftware/mender-server/services/deployments/model"
	fs_mocks "github.com/mendersoftware/mender-server/services/deployments/storage/mocks"
	"github.com/mendersoftware/mender-server/services/deployments/store/mocks"
	h "github.com/mendersoftware/mender-server/services/deployments/utils/testing"
)

type BogusReader struct{}

func (r *BogusReader) Read(b []byte) (int, error) {
	return len(b), nil
}

func TestGenerateImageError(t *testing.T) {
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)

	testCases := []struct {
		multipartGenerateImage *model.MultipartGenerateImageMsg
		expectedError          error
	}{
		{
			multipartGenerateImage: nil,
			expectedError:          ErrModelMultipartUploadMsgMalformed,
		},
	}

	ctx := context.Background()
	for i := range testCases {
		tc := testCases[i]
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			artifactID, err := d.GenerateImage(ctx, tc.multipartGenerateImage)

			assert.Equal(t, artifactID, "")
			assert.Error(t, err)
			assert.EqualError(t, err, tc.expectedError.Error())
		})
	}
}

func TestGenerateImageArtifactIsNotUnique(t *testing.T) {
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)

	db.On("IsArtifactUnique",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("[]string"),
	).Return(false, nil)

	multipartGenerateImage := &model.MultipartGenerateImageMsg{
		Name:                  "name",
		Description:           "description",
		DeviceTypesCompatible: []string{"Beagle Bone"},
		Type:                  "single_file",
		Args:                  "",
		FileReader:            nil,
	}

	ctx := context.Background()
	artifactID, err := d.GenerateImage(ctx, multipartGenerateImage)

	assert.Equal(t, artifactID, "")
	assert.Error(t, err)
	assert.EqualError(t, err, ErrModelArtifactNotUnique.Error())

	db.AssertExpectations(t)
}

func TestGenerateImageErrorWhileCheckingIfArtifactIsNotUnique(t *testing.T) {
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)

	db.On("IsArtifactUnique",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("[]string"),
	).Return(false, errors.New("error"))

	multipartGenerateImage := &model.MultipartGenerateImageMsg{
		Name:                  "name",
		Description:           "description",
		DeviceTypesCompatible: []string{"Beagle Bone"},
		Type:                  "single_file",
		Args:                  "",
		FileReader:            nil,
	}

	ctx := context.Background()
	artifactID, err := d.GenerateImage(ctx, multipartGenerateImage)

	assert.Equal(t, artifactID, "")
	assert.Error(t, err)
	assert.EqualError(t, err, "Fail to check if artifact is unique: error")

	db.AssertExpectations(t)
}

func TestGenerateImageErrorWhileUploading(t *testing.T) {
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)
	ctx := context.Background()

	fs.On("PutObject",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("*bytes.Reader"),
	).Return(errors.New("error while uploading"))

	db.On("GetStorageSettings",
		ctx,
	).Return(nil, nil)

	db.On("IsArtifactUnique",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("[]string"),
	).Return(true, nil)

	multipartGenerateImage := &model.MultipartGenerateImageMsg{
		Name:                  "name",
		Description:           "description",
		DeviceTypesCompatible: []string{"Beagle Bone"},
		Type:                  "single_file",
		Args:                  "",
		FileReader:            bytes.NewReader([]byte("123456790")),
	}

	artifactID, err := d.GenerateImage(ctx, multipartGenerateImage)

	assert.Equal(t, artifactID, "")
	assert.Error(t, err)
	assert.EqualError(t, err, "error while uploading")

	db.AssertExpectations(t)
	fs.AssertExpectations(t)
}

func TestGenerateImageErrorS3GetRequest(t *testing.T) {
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)
	ctx := context.Background()

	fs.On("PutObject",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("*bytes.Reader"),
	).Return(nil)

	db.On("IsArtifactUnique",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("[]string"),
	).Return(true, nil)

	db.On("GetStorageSettings",
		ctx,
	).Return(nil, nil)

	fs.On("GetRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(nil, errors.New("error get request")).
		On("DeleteObject",
			h.ContextMatcher(),
			mock.AnythingOfType("string")).
		Return(errors.New("error!"))

	multipartGenerateImage := &model.MultipartGenerateImageMsg{
		Name:                  "name",
		Description:           "description",
		DeviceTypesCompatible: []string{"Beagle Bone"},
		Type:                  "single_file",
		Args:                  "",
		FileReader:            bytes.NewReader([]byte("123456790")),
	}

	artifactID, err := d.GenerateImage(ctx, multipartGenerateImage)

	assert.Equal(t, artifactID, "")
	assert.Error(t, err)
	assert.EqualError(t, err, "error get request")

	db.AssertExpectations(t)
	fs.AssertExpectations(t)
}

func TestGenerateImageErrorS3DeleteRequest(t *testing.T) {
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)
	ctx := context.Background()

	fs.On("PutObject",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("*bytes.Reader"),
	).Return(nil)

	db.On("IsArtifactUnique",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("[]string"),
	).Return(true, nil)

	db.On("GetStorageSettings",
		ctx,
	).Return(nil, nil)

	fs.On("GetRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(&model.Link{
		Uri: "GET",
	}, nil)

	fs.On("DeleteRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(nil, errors.New("error delete request")).
		On("DeleteObject",
			h.ContextMatcher(),
			mock.AnythingOfType("string")).
		Return(errors.New("error!"))

	multipartGenerateImage := &model.MultipartGenerateImageMsg{
		Name:                  "name",
		Description:           "description",
		DeviceTypesCompatible: []string{"Beagle Bone"},
		Type:                  "single_file",
		Args:                  "",
		FileReader:            bytes.NewReader([]byte("123456790")),
	}

	artifactID, err := d.GenerateImage(ctx, multipartGenerateImage)

	assert.Equal(t, artifactID, "")
	assert.Error(t, err)
	assert.EqualError(t, err, "error delete request")

	db.AssertExpectations(t)
	fs.AssertExpectations(t)
}

func TestGenerateImageErrorWhileStartingWorkflow(t *testing.T) {
	generateErr := errors.New("failed to start workflow: generate_artifact")
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)
	ctx := context.Background()

	fs.On("GetRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(&model.Link{
		Uri: "GET",
	}, nil)

	fs.On("DeleteRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(&model.Link{
		Uri: "DELETE",
	}, nil)

	workflowsClient := oas_client.NewMockWorkflowsOtherAPI(t)
	req := client.ApiStartWorkflowRequest{
		ApiService: workflowsClient,
	}
	workflowsClient.EXPECT().
		StartWorkflow(h.ContextMatcher(), "generate_artifact").
		Return(req)
	workflowsClient.EXPECT().
		StartWorkflowExecute(mock.Anything).
		Return(nil, nil, generateErr)
	d.workflowsClient = workflowsClient

	fs.On("PutObject",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("*bytes.Reader"),
	).Return(nil)

	fs.On("DeleteObject",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
	).Return(nil)

	db.On("IsArtifactUnique",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("[]string"),
	).Return(true, nil)

	db.On("GetStorageSettings",
		ctx,
	).Return(nil, nil)

	multipartGenerateImage := &model.MultipartGenerateImageMsg{
		Name:                  "name",
		Description:           "description",
		DeviceTypesCompatible: []string{"Beagle Bone"},
		Type:                  "single_file",
		Args:                  "",
		FileReader:            bytes.NewReader([]byte("123456790")),
	}

	artifactID, err := d.GenerateImage(ctx, multipartGenerateImage)

	assert.Equal(t, artifactID, "")
	assert.Error(t, err)
	assert.EqualError(t, err, generateErr.Error())

	db.AssertExpectations(t)
	fs.AssertExpectations(t)
	workflowsClient.AssertExpectations(t)
}

func TestGenerateImageErrorWhileStartingWorkflowAndFailsWhenCleaningUp(t *testing.T) {
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)
	ctx := context.Background()

	workflowsClient := oas_client.NewMockWorkflowsOtherAPI(t)
	req := client.ApiStartWorkflowRequest{
		ApiService: workflowsClient,
	}
	workflowsClient.EXPECT().
		StartWorkflow(h.ContextMatcher(), "generate_artifact").
		Return(req)
	workflowsClient.EXPECT().
		StartWorkflowExecute(mock.Anything).
		Return(nil, nil, errors.New("failed to start workflow: generate_artifact"))
	d.workflowsClient = workflowsClient

	fs.On("GetRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(&model.Link{
		Uri: "GET",
	}, nil)

	fs.On("DeleteRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(&model.Link{
		Uri: "DELETE",
	}, nil)

	fs.On("PutObject",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("*bytes.Reader"),
	).Return(nil)

	fs.On("DeleteObject",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
	).Return(errors.New("unable to remove the file"))

	db.On("IsArtifactUnique",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("[]string"),
	).Return(true, nil)

	db.On("GetStorageSettings",
		ctx,
	).Return(nil, nil)

	multipartGenerateImage := &model.MultipartGenerateImageMsg{
		Name:                  "name",
		Description:           "description",
		DeviceTypesCompatible: []string{"Beagle Bone"},
		Type:                  "single_file",
		Args:                  "",
		FileReader:            bytes.NewReader([]byte("123456790")),
	}

	artifactID, err := d.GenerateImage(ctx, multipartGenerateImage)

	assert.Equal(t, artifactID, "")
	assert.Error(t, err)
	assert.EqualError(t, err, "unable to remove the file: failed to start workflow: generate_artifact")

	db.AssertExpectations(t)
	fs.AssertExpectations(t)
	workflowsClient.AssertExpectations(t)
}

func TestGenerateImageSuccessful(t *testing.T) {
	ctx := context.Background()
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)

	multipartGenerateImage := &model.MultipartGenerateImageMsg{
		Name:                  "name",
		Description:           "description",
		DeviceTypesCompatible: []string{"Beagle Bone"},
		Type:                  "single_file",
		Args:                  "args",
		FileReader:            bytes.NewReader([]byte("123456790")),
	}

	workflowsClient := oas_client.NewMockWorkflowsOtherAPI(t)
	d.workflowsClient = workflowsClient
	req := client.ApiStartWorkflowRequest{
		ApiService: workflowsClient,
	}
	workflowsClient.EXPECT().
		StartWorkflow(h.ContextMatcher(), "generate_artifact").
		Run(func(ctx context.Context, name string) {
			// Add the expectation for the chained call here as
			// tenant ID is set post call.
			expected := req.RequestBody(multipartGenerateImage.ToRequestParams())
			workflowsClient.EXPECT().
				StartWorkflowExecute(expected).
				Return(
					&client.StartWorkflow201Response{Id: types.Pointer("123")},
					&http.Response{StatusCode: 201, Body: io.NopCloser(nil)},
					nil,
				).
				Once()
		}).
		Return(req).
		Once()
	fs.On("GetRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(&model.Link{
		Uri: "GET",
	}, nil)

	fs.On("DeleteRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(&model.Link{
		Uri: "DELETE",
	}, nil)

	fs.On("PutObject",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("*bytes.Reader"),
	).Return(nil)

	db.On("IsArtifactUnique",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("[]string"),
	).Return(true, nil)

	db.On("GetStorageSettings",
		ctx,
	).Return(nil, nil)

	artifactID, err := d.GenerateImage(ctx, multipartGenerateImage)

	assert.NotEqual(t, artifactID, "")
	assert.Nil(t, err)

	db.AssertExpectations(t)
	fs.AssertExpectations(t)
	workflowsClient.AssertExpectations(t)
}

func TestGenerateImageSuccessfulWithTenant(t *testing.T) {
	ctx := context.Background()
	db := mocks.DataStore{}
	fs := &fs_mocks.ObjectStorage{}
	d := NewDeployments(&db, fs, 0, false)

	multipartGenerateImage := &model.MultipartGenerateImageMsg{
		Name:                  "name",
		Description:           "description",
		DeviceTypesCompatible: []string{"Beagle Bone"},
		Type:                  "single_file",
		Args:                  "args",
		FileReader:            bytes.NewReader([]byte("123456790")),
	}

	workflowsClient := oas_client.NewMockWorkflowsOtherAPI(t)
	d.workflowsClient = workflowsClient
	req := client.ApiStartWorkflowRequest{
		ApiService: workflowsClient,
	}
	workflowsClient.EXPECT().
		StartWorkflow(h.ContextMatcher(), "generate_artifact").
		Run(func(ctx context.Context, name string) {
			// Add the expectation for the chained call here as
			// tenant ID is set post call.
			expected := req.RequestBody(multipartGenerateImage.ToRequestParams())
			workflowsClient.EXPECT().
				StartWorkflowExecute(expected).
				Return(
					&client.StartWorkflow201Response{Id: types.Pointer("123")},
					&http.Response{StatusCode: 201, Body: io.NopCloser(nil)},
					nil,
				).
				Once()
		}).
		Return(req).
		Once()

	fs.On("GetRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(&model.Link{
		Uri: "GET",
	}, nil)

	fs.On("DeleteRequest",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("time.Duration"),
		false,
	).Return(&model.Link{
		Uri: "DELETE",
	}, nil)

	fs.On("PutObject",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("*bytes.Reader"),
	).Return(nil)

	db.On("IsArtifactUnique",
		h.ContextMatcher(),
		mock.AnythingOfType("string"),
		mock.AnythingOfType("[]string"),
	).Return(true, nil)

	identityObject := &identity.Identity{Tenant: "tenant_id"}
	ctxWithIdentity := identity.WithContext(ctx, identityObject)

	db.On("GetStorageSettings",
		ctxWithIdentity,
	).Return(nil, nil)

	artifactID, err := d.GenerateImage(ctxWithIdentity, multipartGenerateImage)

	assert.NotEqual(t, artifactID, "")
	assert.Nil(t, err)

	db.AssertExpectations(t)
	fs.AssertExpectations(t)
	workflowsClient.AssertExpectations(t)
}

func TestGenerateConfigurationImage(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		Name string

		DeviceType   string
		DeploymentID string

		StoreError error
		Deployment *model.Deployment

		Error error
	}{{
		Name: "ok",

		DeviceType: "strawberryPlanck",
		DeploymentID: uuid.NewSHA1(
			uuid.NameSpaceOID,
			[]byte("deployment"),
		).String(),
		Deployment: &model.Deployment{
			Id: uuid.NewSHA1(
				uuid.NameSpaceOID,
				[]byte("deployment"),
			).String(),
			Type:          model.DeploymentTypeConfiguration,
			Configuration: []byte("{\"foo\":\"bar\"}"),
			DeploymentConstructor: &model.DeploymentConstructor{
				Name:         "spicyDeployment",
				ArtifactName: "spicyPi",
			},
		},
	}, {
		Name: "error, internal DataStore error",

		DeviceType: "strawberryPlanck",
		DeploymentID: uuid.NewSHA1(
			uuid.NameSpaceOID,
			[]byte("deployment"),
		).String(),
		StoreError: errors.New("internal error"),
		Error:      errors.New("internal error"),
	}, {
		Name: "error, deployment not found",

		DeviceType: "strawberryPlanck",
		DeploymentID: uuid.NewSHA1(
			uuid.NameSpaceOID,
			[]byte("deployment"),
		).String(),
		Error: ErrModelDeploymentNotFound,
	}, {
		Name: "error, invalid JSON metadata",

		DeviceType: "strawberryPlanck",
		DeploymentID: uuid.NewSHA1(
			uuid.NameSpaceOID,
			[]byte("deployment"),
		).String(),
		Deployment: &model.Deployment{
			Id: uuid.NewSHA1(
				uuid.NameSpaceOID,
				[]byte("deployment"),
			).String(),
			Type:          model.DeploymentTypeConfiguration,
			Configuration: []byte("gotcha"),
			DeploymentConstructor: &model.DeploymentConstructor{
				Name:         "spicyDeployment",
				ArtifactName: "spicyPi",
			},
		},
		Error: errors.New(
			"malformed configuration in deployment: " +
				"invalid character 'g' looking for beginning of value",
		),
	}}
	for i := range testCases {
		tc := testCases[i]
		t.Run(tc.Name, func(t *testing.T) {
			t.Parallel()
			ctx := context.Background()
			ds := new(mocks.DataStore)
			defer ds.AssertExpectations(t)
			ds.On("FindDeploymentByID", ctx, tc.DeploymentID).
				Return(tc.Deployment, tc.StoreError)
			d := NewDeployments(ds, nil, 0, false)
			artieFact, err := d.GenerateConfigurationImage(
				ctx, tc.DeviceType, tc.DeploymentID,
			)
			if tc.Error != nil {
				assert.EqualError(t, err, tc.Error.Error())
			} else {
				artieReader := areader.NewReader(artieFact)
				err := artieReader.ReadArtifactHeaders()
				if !assert.NoError(t, err, "Generated artifact is invalid") {
					t.FailNow()
				}
				assert.Equal(t,
					[]artifact.UpdateType{{Type: &ArtifactConfigureType}},
					artieReader.GetUpdates(),
				)
				provides, _ := artieReader.MergeArtifactProvides()
				if assert.Contains(t,
					provides,
					ArtifactConfigureProvides,
				) {
					assert.Equal(t,
						tc.Deployment.ArtifactName,
						provides[ArtifactConfigureProvides],
					)
				}
				depends, _ := artieReader.MergeArtifactDepends()
				if assert.Contains(t, depends, "device_type") {
					deviceTypes := []interface{}{tc.DeviceType}
					assert.Equal(t,
						deviceTypes,
						depends["device_type"],
					)
				}
				handlers := artieReader.GetHandlers()
				if assert.Len(t, handlers, 1) && assert.Contains(t, handlers, 0) {
					handler := handlers[0]
					metadata, _ := handler.GetUpdateMetaData()
					var actual map[string]interface{}
					//nolint:errcheck
					json.Unmarshal(tc.Deployment.Configuration, &actual)
					assert.Equal(t, metadata, actual)
				}
			}
		})
	}

}
func strTotimePtr(timeStr string) *time.Time {
	t, _ := time.Parse(time.RFC3339, timeStr)
	t = t.UTC()
	return &t
}

var testImgs = []*model.Image{
	{
		Id: "6d4f6e27-c3bb-438c-ad9c-d9de30e59d80",
		ImageMeta: &model.ImageMeta{
			Description: "description",
		},

		ArtifactMeta: &model.ArtifactMeta{
			Name:                  "App1 v1.0",
			DeviceTypesCompatible: []string{"foo"},
			Updates:               []model.Update{},
		},
		Modified: strTotimePtr("2010-09-22T22:00:00+00:00"),
	},
	{
		Id: "6d4f6e27-c3bb-438c-ad9c-d9de30e59d81",
		ImageMeta: &model.ImageMeta{
			Description: "description",
		},

		ArtifactMeta: &model.ArtifactMeta{
			Name:                  "App2 v0.1",
			DeviceTypesCompatible: []string{"foo"},
			Updates:               []model.Update{},
		},
		Modified: strTotimePtr("2010-09-22T22:02:00+00:00"),
	},
	{
		Id: "6d4f6e27-c3bb-438c-ad9c-d9de30e59d82",
		ImageMeta: &model.ImageMeta{
			Description: "description",
		},

		ArtifactMeta: &model.ArtifactMeta{
			Name:                  "App1 v1.0",
			DeviceTypesCompatible: []string{"bar, baz"},
			Updates:               []model.Update{},
		},
		Modified: strTotimePtr("2010-09-22T22:01:00+00:00"),
	},
	{
		Id: "6d4f6e27-c3bb-438c-ad9c-d9de30e59d83",
		ImageMeta: &model.ImageMeta{
			Description: "description",
		},

		ArtifactMeta: &model.ArtifactMeta{
			Name:                  "App1 v1.0",
			DeviceTypesCompatible: []string{"bork"},
			Updates:               []model.Update{},
		},
		Modified: strTotimePtr("2010-09-22T22:04:00+00:00"),
	},
	{
		Id: "6d4f6e27-c3bb-438c-ad9c-d9de30e59d84",
		ImageMeta: &model.ImageMeta{
			Description: "extended description",
		},

		ArtifactMeta: &model.ArtifactMeta{
			Name:                  "App2 v0.1",
			DeviceTypesCompatible: []string{"bar", "baz"},
			Updates:               []model.Update{},
		},
		Modified: strTotimePtr("2010-09-22T22:03:00+00:00"),
	},
}

func TestListImages(t *testing.T) {
	testCases := []struct {
		Name string

		Images     []*model.Image
		StoreError error
	}{
		{
			Name:       "ok",
			Images:     testImgs,
			StoreError: nil,
		},
		{
			Name:   "ok, no image",
			Images: []*model.Image{},
		},
		{
			Name:       "error",
			Images:     nil,
			StoreError: errors.New("some error"),
		},
	}

	for i := range testCases {
		tc := testCases[i]
		t.Run(tc.Name, func(t *testing.T) {
			t.Parallel()
			ctx := context.Background()
			ds := new(mocks.DataStore)
			defer ds.AssertExpectations(t)
			imgFilter := model.ReleaseOrImageFilter{}
			ds.On("ListImages", ctx, &imgFilter).
				Return(tc.Images, len(tc.Images), tc.StoreError)

			d := NewDeployments(ds, nil, 0, false)

			images, count, err := d.ListImages(ctx, &imgFilter)

			assert.Equal(t, tc.Images, images)
			assert.Equal(t, len(tc.Images), count)
			assert.ErrorIs(t, err, tc.StoreError)
		})
	}
}

func TestListImagesV2(t *testing.T) {
	testCases := []struct {
		Name string

		Images     []*model.Image
		StoreError error
	}{
		{
			Name:       "ok",
			Images:     testImgs,
			StoreError: nil,
		},
		{
			Name:   "ok, no image",
			Images: []*model.Image{},
		},
		{
			Name:       "error",
			Images:     nil,
			StoreError: errors.New("some error"),
		},
	}

	for i := range testCases {
		tc := testCases[i]
		t.Run(tc.Name, func(t *testing.T) {
			t.Parallel()
			ctx := context.Background()
			ds := new(mocks.DataStore)
			defer ds.AssertExpectations(t)
			imgFilter := model.ImageFilter{}
			ds.On("ListImagesV2", ctx, &imgFilter).
				Return(tc.Images, tc.StoreError)

			d := NewDeployments(ds, nil, 0, false)

			images, err := d.ListImagesV2(ctx, &imgFilter)

			assert.Equal(t, tc.Images, images)
			assert.Equal(t, len(tc.Images), len(images))
			assert.ErrorIs(t, err, tc.StoreError)
		})
	}
}
