//
// Copyright 2024 The Chainloop 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 biz_test

import (
	"context"
	"testing"

	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
	"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/testhelpers"
	backends "github.com/chainloop-dev/chainloop/pkg/blobmanager"
	blobM "github.com/chainloop-dev/chainloop/pkg/blobmanager/mocks"
	"github.com/chainloop-dev/chainloop/pkg/blobmanager/oci"
	creds "github.com/chainloop-dev/chainloop/pkg/credentials/mocks"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
)

const location = "my-location"
const backendType = oci.ProviderID

var description = "my-description"

func (s *CASBackendIntegrationTestSuite) TestUniqueNameDuringCreate() {
	orgID, err := uuid.Parse(s.orgOne.ID)
	require.NoError(s.T(), err)

	testCases := []struct {
		name       string
		opts       *biz.CASBackendCreateOpts
		wantErrMsg string
	}{
		{
			name:       "org missing",
			opts:       &biz.CASBackendCreateOpts{Name: "name"},
			wantErrMsg: "required",
		},
		{
			name:       "name missing",
			opts:       &biz.CASBackendCreateOpts{CASBackendOpts: &biz.CASBackendOpts{OrgID: orgID}},
			wantErrMsg: "required",
		},
		{
			name:       "invalid name",
			opts:       &biz.CASBackendCreateOpts{Name: "this/not/valid", CASBackendOpts: &biz.CASBackendOpts{OrgID: orgID}},
			wantErrMsg: "RFC 1123",
		},
		{
			name:       "another invalid name",
			opts:       &biz.CASBackendCreateOpts{Name: "this-not valid", CASBackendOpts: &biz.CASBackendOpts{OrgID: orgID}},
			wantErrMsg: "RFC 1123",
		},
		{
			name: "can create it with just the name and the org",
			opts: &biz.CASBackendCreateOpts{Name: "name", CASBackendOpts: &biz.CASBackendOpts{OrgID: orgID}},
		},
	}

	for _, tc := range testCases {
		s.Run(tc.name, func() {
			var orgID string
			if tc.opts.CASBackendOpts != nil && tc.opts.OrgID != uuid.Nil {
				orgID = tc.opts.OrgID.String()
			}

			got, err := s.CASBackend.Create(context.Background(), orgID, tc.opts.Name, location, description, backendType, nil, true, nil)
			if tc.wantErrMsg != "" {
				s.ErrorContains(err, tc.wantErrMsg)
				return
			}

			require.NoError(s.T(), err)
			s.NotEmpty(got.ID)
			s.Equal(tc.opts.Name, got.Name)
		})
	}
}

func (s *CASBackendIntegrationTestSuite) TestCreate() {
	assert := assert.New(s.T())
	orgID := s.orgOne.ID

	s.Run("non-existing org", func() {
		_, err := s.CASBackend.Create(
			context.TODO(), uuid.NewString(), randomName(), location, description, backendType, nil, true, nil,
		)
		assert.Error(err)
	})

	s.Run("create default", func() {
		b, err := s.CASBackend.Create(context.TODO(), orgID, "my-name", location, description, backendType, nil, true, nil)
		assert.NoError(err)

		if diff := cmp.Diff(&biz.CASBackend{
			Location:         location,
			Name:             "my-name",
			Description:      description,
			SecretName:       "stored-OCI-secret",
			Provider:         backendType,
			ValidationStatus: "OK",
			Default:          true,
			Inline:           false,
			Limits: &biz.CASBackendLimits{
				MaxBytes: 104857600,
			},
		}, b,
			cmpopts.IgnoreFields(biz.CASBackend{}, "CreatedAt", "ID", "ValidatedAt", "OrganizationID"),
		); diff != "" {
			assert.Failf("mismatch (-want +got):\n%s", diff)
		}
	})

	s.Run("create fallback", func() {
		b, err := s.CASBackend.CreateInlineFallbackBackend(context.TODO(), orgID)
		assert.NoError(err)

		if diff := cmp.Diff(&biz.CASBackend{
			Description:      "Embed artifacts content in the attestation (fallback)",
			Provider:         biz.CASBackendInline,
			Name:             "default-inline",
			Default:          true,
			Inline:           true,
			Fallback:         true,
			ValidationStatus: "OK",
			Limits: &biz.CASBackendLimits{
				MaxBytes: 512000,
			},
		}, b,
			cmpopts.IgnoreFields(biz.CASBackend{}, "CreatedAt", "ID", "ValidatedAt", "OrganizationID"),
		); diff != "" {
			assert.Failf("mismatch (-want +got):\n%s", diff)
		}
	})
}
func (s *CASBackendIntegrationTestSuite) TestCreateOverride() {
	assert := assert.New(s.T())

	// When a new default backend is created, the previous default should be overridden
	b1, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil)
	assert.NoError(err)
	assert.True(b1.Default)
	b2, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), "another-location", description, backendType, nil, true, nil)
	assert.NoError(err)
	assert.True(b2.Default)

	// Check that the first one is no longer default
	b1, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, b1.ID.String())
	assert.NoError(err)
	assert.False(b1.Default)
}

func (s *CASBackendIntegrationTestSuite) TestFindByNameInOrg() {
	name1 := randomName()
	name2 := randomName()
	b1, err := s.CASBackend.Create(context.TODO(), s.orgOne.ID, name1, location, description, backendType, nil, true, nil)
	s.NoError(err)
	s.True(b1.Default)
	b2, err := s.CASBackend.Create(context.TODO(), s.orgTwo.ID, name2, "another-location", description, backendType, nil, true, nil)
	s.NoError(err)

	testCases := []struct {
		name          string
		orgID         string
		backendName   string
		wantBackendID uuid.UUID
		wantErr       bool
	}{
		{
			name:        "non-existent org",
			orgID:       uuid.New().String(),
			backendName: name1,
			wantErr:     true,
		},
		{
			name:        "non-existent name",
			orgID:       s.orgOne.ID,
			backendName: "non-existent",
			wantErr:     true,
		},
		{
			name:          "existing name",
			orgID:         s.orgOne.ID,
			backendName:   name1,
			wantBackendID: b1.ID,
			wantErr:       false,
		},
		{
			name:        "existing name in another org",
			orgID:       s.orgTwo.ID,
			backendName: name1,
			wantErr:     true,
		},
		{
			name:          "existing name in another org",
			orgID:         s.orgTwo.ID,
			backendName:   name2,
			wantBackendID: b2.ID,
			wantErr:       false,
		},
	}

	for _, tc := range testCases {
		s.Run(tc.name, func() {
			got, err := s.CASBackend.FindByNameInOrg(context.Background(), tc.orgID, tc.backendName)
			if tc.wantErr {
				s.Error(err)
				return
			}

			s.NoError(err)
			s.Equal(tc.wantBackendID, got.ID)
		})
	}
}

func (s *CASBackendIntegrationTestSuite) TestUpdate() {
	assert := assert.New(s.T())

	s.Run("overrides previous backends", func() {
		// When a new default backend is set, the previous default should be overridden
		defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil)
		assert.NoError(err)
		assert.True(defaultB.Default)
		nonDefaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), "another-location", description, backendType, nil, false, nil)
		assert.NoError(err)
		assert.False(nonDefaultB.Default)

		// Update the non-default to be default
		nonDefaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, nonDefaultB.ID.String(), toPtrS(""), nil, toPtrBool(true), nil)
		assert.NoError(err)
		assert.True(nonDefaultB.Default)

		// Check that the first one is no longer default
		defaultB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String())
		assert.NoError(err)
		assert.False(defaultB.Default)
	})

	s.Run("can update only the description", func() {
		// When a new default backend is set, the previous default should be overridden
		defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil)
		assert.NoError(err)
		assert.Equal(description, defaultB.Description)

		// Update the description
		defaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String(), toPtrS("updated desc"), nil, toPtrBool(true), nil)
		assert.NoError(err)
		assert.Equal("updated desc", defaultB.Description)
		assert.True(defaultB.Default)
	})

	s.Run("can update only the status", func() {
		// When a new default backend is set, the previous default should be overridden
		defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil)
		assert.NoError(err)
		assert.Equal(description, defaultB.Description)

		// update the status
		defaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String(), toPtrS(description), nil, toPtrBool(false), nil)
		assert.NoError(err)
		assert.Equal(description, defaultB.Description)
		assert.False(defaultB.Default)
	})

	s.Run("the fallback backend will be set if default true => false", func() {
		// When a new default backend is set, the previous default should be overridden
		fallbackB, err := s.CASBackend.CreateInlineFallbackBackend(context.TODO(), s.orgNoBackend.ID)
		assert.NoError(err)
		assert.True(fallbackB.Fallback)
		assert.True(fallbackB.Default)

		// Create a new default backend
		defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil)
		assert.NoError(err)
		assert.False(defaultB.Fallback) // it's not fallback
		assert.True(defaultB.Default)

		// The fallback now is no longer the default
		fallbackB, err = s.CASBackend.FindFallbackBackend(context.TODO(), s.orgNoBackend.ID)
		assert.NoError(err)
		assert.False(fallbackB.Default)

		// update the status
		defaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String(), toPtrS(description), nil, toPtrBool(false), nil)
		assert.NoError(err)
		assert.False(defaultB.Default)

		// The fallback is now the default
		fallbackB, err = s.CASBackend.FindFallbackBackend(context.TODO(), s.orgNoBackend.ID)
		assert.NoError(err)
		assert.True(fallbackB.Default)
	})

	s.Run("can rotate credentials", func() {
		// When a new default backend is set, the previous default should be overridden
		defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil)
		assert.NoError(err)
		assert.Equal(description, defaultB.Description)

		// update the secret
		creds := struct{}{}
		ctx := context.TODO()
		s.credsWriter.Mock = mock.Mock{}
		s.credsWriter.On("SaveCredentials", ctx, s.orgNoBackend.ID, creds).Return("new-secret", nil)
		s.credsWriter.On("ReadCredentials", ctx, "new-secret", mock.Anything).Return(nil)
		s.backendProvider.On("ValidateAndExtractCredentials", location, mock.Anything).Return(nil, nil)
		defaultB, err = s.CASBackend.Update(ctx, s.orgNoBackend.ID, defaultB.ID.String(), toPtrS(description), creds, nil, nil)
		assert.NoError(err)
		assert.Equal(description, defaultB.Description)
		assert.Equal("new-secret", defaultB.SecretName)
		assert.True(defaultB.Default)
	})
}

func (s *CASBackendIntegrationTestSuite) TestSoftDelete() {
	assert := assert.New(s.T())
	ctx := context.TODO()

	backends, err := s.CASBackend.List(ctx, s.orgTwo.ID)
	assert.NoError(err)
	// There are two backends
	require.Len(s.T(), backends, 2)

	// We are going to delete the default one
	toDelete := backends[1].ID
	assert.True(backends[1].Default)

	// Delete it
	err = s.CASBackend.SoftDelete(ctx, s.orgTwo.ID, toDelete.String())
	assert.NoError(err)

	// there is one left
	backends, err = s.CASBackend.List(ctx, s.orgTwo.ID)
	assert.NoError(err)
	// There is one backend
	require.Len(s.T(), backends, 1)
	assert.Equal(backends[0].ID, s.casBackend3.ID)

	// the deleted one can not be found by ID either
	_, err = s.CASBackend.FindByIDInOrg(ctx, s.orgTwo.ID, toDelete.String())
	assert.ErrorAs(err, &biz.ErrNotFound{})

	// the deleted one can not be found by as default
	_, err = s.CASBackend.FindDefaultBackend(ctx, s.orgTwo.ID)
	assert.ErrorAs(err, &biz.ErrNotFound{})
}

func (s *CASBackendIntegrationTestSuite) TestSoftDeleteFallbackOverride() {
	assert := assert.New(s.T())
	// We have two backends, one is fallback and another is default
	fallbackB, err := s.CASBackend.CreateInlineFallbackBackend(context.TODO(), s.orgNoBackend.ID)
	assert.NoError(err)
	assert.True(fallbackB.Default)

	// When a new default backend is set, the previous default should be overridden
	b, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil)
	assert.NoError(err)

	// The fallback is not the default anymore
	fallbackB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, fallbackB.ID.String())
	assert.NoError(err)
	assert.False(fallbackB.Default)

	// Once we delete the default, the fallback should be the default again
	// Delete it
	err = s.CASBackend.SoftDelete(context.TODO(), s.orgNoBackend.ID, b.ID.String())
	assert.NoError(err)

	// The fallback is NOW THE DEFAULT
	fallbackB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, fallbackB.ID.String())
	assert.NoError(err)
	assert.True(fallbackB.Default)
}

func (s *CASBackendIntegrationTestSuite) TestList() {
	testCases := []struct {
		name           string
		orgID          string
		expectedResult []*biz.CASBackend
	}{
		{
			name:           "non-existent org",
			orgID:          uuid.New().String(),
			expectedResult: []*biz.CASBackend{},
		},
		{
			name:           "no backends for org",
			orgID:          s.orgNoBackend.ID,
			expectedResult: []*biz.CASBackend{},
		},
		{
			name:  "one backend for org",
			orgID: s.orgOne.ID,
			expectedResult: []*biz.CASBackend{
				s.casBackend1,
			},
		},
		{
			name:  "2 backends in org",
			orgID: s.orgTwo.ID,
			expectedResult: []*biz.CASBackend{
				s.casBackend3,
				s.casBackend2,
			},
		},
	}

	for _, tc := range testCases {
		s.Run(tc.name, func() {
			assert := assert.New(s.T())
			ctx := context.Background()
			backends, err := s.CASBackend.List(ctx, tc.orgID)
			assert.NoError(err)
			assert.Equal(tc.expectedResult, backends)
		})
	}
}

func (s *CASBackendIntegrationTestSuite) SetupTest() {
	var err error
	assert := assert.New(s.T())
	ctx := context.Background()
	// OCI repository credentials
	s.credsWriter = creds.NewReaderWriter(s.T())
	s.credsWriter.On(
		"SaveCredentials", mock.Anything, mock.Anything, mock.Anything,
	).Return("stored-OCI-secret", nil)

	// Create backend provider mock
	s.backendProvider = blobM.NewProvider(s.T())

	s.TestingUseCases = testhelpers.NewTestingUseCases(s.T(),
		testhelpers.WithCredsReaderWriter(s.credsWriter),
		testhelpers.WithBackendProviders(backends.Providers{
			"OCI": s.backendProvider,
		}))

	s.orgOne, err = s.Organization.Create(ctx, "testing-org-1-with-one-backend")
	assert.NoError(err)
	s.orgTwo, err = s.Organization.Create(ctx, "testing-org-2-with-2-backends")
	assert.NoError(err)
	s.orgNoBackend, err = s.Organization.Create(ctx, "testing-org-3-no-backends")
	assert.NoError(err)

	s.casBackend1, err = s.CASBackend.Create(ctx, s.orgOne.ID, randomName(), "my-location", "backend 1 description", backendType, nil, true, nil)
	assert.NoError(err)
	s.casBackend2, err = s.CASBackend.Create(ctx, s.orgTwo.ID, randomName(), "my-location 2", "backend 2 description", backendType, nil, true, nil)
	assert.NoError(err)
	s.casBackend3, err = s.CASBackend.Create(ctx, s.orgTwo.ID, randomName(), "my-location 3", "backend 3 description", backendType, nil, false, nil)
	assert.NoError(err)
}

func TestCASBackendUseCase(t *testing.T) {
	suite.Run(t, new(CASBackendIntegrationTestSuite))
}

type CASBackendIntegrationTestSuite struct {
	testhelpers.UseCasesEachTestSuite
	orgTwo, orgOne, orgNoBackend          *biz.Organization
	casBackend1, casBackend2, casBackend3 *biz.CASBackend
	credsWriter                           *creds.ReaderWriter
	backendProvider                       *blobM.Provider
}

func TestIntegrationCASBackend(t *testing.T) {
	suite.Run(t, new(CASBackendIntegrationTestSuite))
}
