// Copyright 2025 Cloudbase Solutions SRL
//
//    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 sql

import (
	"context"
	"encoding/json"
	"fmt"
	"testing"

	"github.com/stretchr/testify/suite"

	commonParams "github.com/cloudbase/garm-provider-common/params"
	dbCommon "github.com/cloudbase/garm/database/common"
	"github.com/cloudbase/garm/database/watcher"
	garmTesting "github.com/cloudbase/garm/internal/testing"
	"github.com/cloudbase/garm/params"
)

type ScaleSetsTestSuite struct {
	suite.Suite
	Store    dbCommon.Store
	adminCtx context.Context
	creds    params.ForgeCredentials

	org        params.Organization
	repo       params.Repository
	enterprise params.Enterprise

	orgEntity        params.ForgeEntity
	repoEntity       params.ForgeEntity
	enterpriseEntity params.ForgeEntity
}

func (s *ScaleSetsTestSuite) SetupTest() {
	// create testing sqlite database
	ctx := context.Background()
	watcher.InitWatcher(ctx)

	db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T()))
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to create db connection: %s", err))
	}
	s.Store = db

	adminCtx := garmTesting.ImpersonateAdminContext(ctx, db, s.T())
	s.adminCtx = adminCtx

	githubEndpoint := garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T())
	s.creds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), githubEndpoint)

	// create an organization for testing purposes
	s.org, err = s.Store.CreateOrganization(s.adminCtx, "test-org", s.creds, "test-webhookSecret", params.PoolBalancerTypeRoundRobin)
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to create org: %s", err))
	}

	s.repo, err = s.Store.CreateRepository(s.adminCtx, "test-org", "test-repo", s.creds, "test-webhookSecret", params.PoolBalancerTypeRoundRobin)
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to create repo: %s", err))
	}

	s.enterprise, err = s.Store.CreateEnterprise(s.adminCtx, "test-enterprise", s.creds, "test-webhookSecret", params.PoolBalancerTypeRoundRobin)
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to create enterprise: %s", err))
	}

	s.orgEntity, err = s.org.GetEntity()
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to get org entity: %s", err))
	}

	s.repoEntity, err = s.repo.GetEntity()
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to get repo entity: %s", err))
	}

	s.enterpriseEntity, err = s.enterprise.GetEntity()
	if err != nil {
		s.FailNow(fmt.Sprintf("failed to get enterprise entity: %s", err))
	}

	s.T().Cleanup(func() {
		err := s.Store.DeleteOrganization(s.adminCtx, s.org.ID)
		if err != nil {
			s.FailNow(fmt.Sprintf("failed to delete org: %s", err))
		}
		err = s.Store.DeleteRepository(s.adminCtx, s.repo.ID)
		if err != nil {
			s.FailNow(fmt.Sprintf("failed to delete repo: %s", err))
		}
		err = s.Store.DeleteEnterprise(s.adminCtx, s.enterprise.ID)
		if err != nil {
			s.FailNow(fmt.Sprintf("failed to delete enterprise: %s", err))
		}
	})
}

func (s *ScaleSetsTestSuite) TearDownTest() {
	watcher.CloseWatcher()
}

func (s *ScaleSetsTestSuite) callback(old, newSet params.ScaleSet) error {
	s.Require().Equal(old.Name, "test-scaleset")
	s.Require().Equal(newSet.Name, "test-scaleset-updated")
	s.Require().Equal(old.OSType, commonParams.Linux)
	s.Require().Equal(newSet.OSType, commonParams.Windows)
	s.Require().Equal(old.OSArch, commonParams.Amd64)
	s.Require().Equal(newSet.OSArch, commonParams.Arm64)
	s.Require().Equal(old.ExtraSpecs, json.RawMessage(`{"test": 1}`))
	s.Require().Equal(newSet.ExtraSpecs, json.RawMessage(`{"test": 111}`))
	s.Require().Equal(old.MaxRunners, uint(10))
	s.Require().Equal(newSet.MaxRunners, uint(60))
	s.Require().Equal(old.MinIdleRunners, uint(5))
	s.Require().Equal(newSet.MinIdleRunners, uint(50))
	s.Require().Equal(old.Image, "test-image")
	s.Require().Equal(newSet.Image, "new-test-image")
	s.Require().Equal(old.Flavor, "test-flavor")
	s.Require().Equal(newSet.Flavor, "new-test-flavor")
	s.Require().Equal(old.GitHubRunnerGroup, "test-group")
	s.Require().Equal(newSet.GitHubRunnerGroup, "new-test-group")
	s.Require().Equal(old.RunnerPrefix.Prefix, "garm")
	s.Require().Equal(newSet.RunnerPrefix.Prefix, "test-prefix2")
	s.Require().Equal(old.Enabled, false)
	s.Require().Equal(newSet.Enabled, true)
	return nil
}

func (s *ScaleSetsTestSuite) TestScaleSetOperations() {
	// create a scale set for the organization
	createScaleSetPrams := params.CreateScaleSetParams{
		Name:              "test-scaleset",
		ProviderName:      "test-provider",
		MaxRunners:        10,
		MinIdleRunners:    5,
		Image:             "test-image",
		Flavor:            "test-flavor",
		OSType:            commonParams.Linux,
		OSArch:            commonParams.Amd64,
		ExtraSpecs:        json.RawMessage(`{"test": 1}`),
		GitHubRunnerGroup: "test-group",
	}

	var orgScaleSet params.ScaleSet
	var repoScaleSet params.ScaleSet
	var enterpriseScaleSet params.ScaleSet
	var err error

	s.T().Run("create org scaleset", func(_ *testing.T) {
		orgScaleSet, err = s.Store.CreateEntityScaleSet(s.adminCtx, s.orgEntity, createScaleSetPrams)
		s.Require().NoError(err)
		s.Require().NotNil(orgScaleSet)
		s.Require().Equal(orgScaleSet.Name, createScaleSetPrams.Name)
		s.T().Cleanup(func() {
			err := s.Store.DeleteScaleSetByID(s.adminCtx, orgScaleSet.ID)
			if err != nil {
				s.FailNow(fmt.Sprintf("failed to delete scaleset: %s", err))
			}
		})
	})

	s.T().Run("create repo scaleset", func(_ *testing.T) {
		repoScaleSet, err = s.Store.CreateEntityScaleSet(s.adminCtx, s.repoEntity, createScaleSetPrams)
		s.Require().NoError(err)
		s.Require().NotNil(repoScaleSet)
		s.Require().Equal(repoScaleSet.Name, createScaleSetPrams.Name)
		s.T().Cleanup(func() {
			err := s.Store.DeleteScaleSetByID(s.adminCtx, repoScaleSet.ID)
			if err != nil {
				s.FailNow(fmt.Sprintf("failed to delete scaleset: %s", err))
			}
		})
	})

	s.T().Run("create enterprise scaleset", func(_ *testing.T) {
		enterpriseScaleSet, err = s.Store.CreateEntityScaleSet(s.adminCtx, s.enterpriseEntity, createScaleSetPrams)
		s.Require().NoError(err)
		s.Require().NotNil(enterpriseScaleSet)
		s.Require().Equal(enterpriseScaleSet.Name, createScaleSetPrams.Name)

		s.T().Cleanup(func() {
			err := s.Store.DeleteScaleSetByID(s.adminCtx, enterpriseScaleSet.ID)
			if err != nil {
				s.FailNow(fmt.Sprintf("failed to delete scaleset: %s", err))
			}
		})
	})

	s.T().Run("create list all scalesets", func(_ *testing.T) {
		allScaleSets, err := s.Store.ListAllScaleSets(s.adminCtx)
		s.Require().NoError(err)
		s.Require().NotEmpty(allScaleSets)
		s.Require().Len(allScaleSets, 3)
	})

	s.T().Run("list repo scalesets", func(_ *testing.T) {
		repoScaleSets, err := s.Store.ListEntityScaleSets(s.adminCtx, s.repoEntity)
		s.Require().NoError(err)
		s.Require().NotEmpty(repoScaleSets)
		s.Require().Len(repoScaleSets, 1)
	})

	s.T().Run("list org scalesets", func(_ *testing.T) {
		orgScaleSets, err := s.Store.ListEntityScaleSets(s.adminCtx, s.orgEntity)
		s.Require().NoError(err)
		s.Require().NotEmpty(orgScaleSets)
		s.Require().Len(orgScaleSets, 1)
	})

	s.T().Run("list enterprise scalesets", func(_ *testing.T) {
		enterpriseScaleSets, err := s.Store.ListEntityScaleSets(s.adminCtx, s.enterpriseEntity)
		s.Require().NoError(err)
		s.Require().NotEmpty(enterpriseScaleSets)
		s.Require().Len(enterpriseScaleSets, 1)
	})

	s.T().Run("get repo scaleset by ID", func(_ *testing.T) {
		repoScaleSetByID, err := s.Store.GetScaleSetByID(s.adminCtx, repoScaleSet.ID)
		s.Require().NoError(err)
		s.Require().NotNil(repoScaleSetByID)
		s.Require().Equal(repoScaleSetByID.ID, repoScaleSet.ID)
		s.Require().Equal(repoScaleSetByID.Name, repoScaleSet.Name)
	})

	s.T().Run("get org scaleset by ID", func(_ *testing.T) {
		orgScaleSetByID, err := s.Store.GetScaleSetByID(s.adminCtx, orgScaleSet.ID)
		s.Require().NoError(err)
		s.Require().NotNil(orgScaleSetByID)
		s.Require().Equal(orgScaleSetByID.ID, orgScaleSet.ID)
		s.Require().Equal(orgScaleSetByID.Name, orgScaleSet.Name)
	})

	s.T().Run("get enterprise scaleset by ID", func(_ *testing.T) {
		enterpriseScaleSetByID, err := s.Store.GetScaleSetByID(s.adminCtx, enterpriseScaleSet.ID)
		s.Require().NoError(err)
		s.Require().NotNil(enterpriseScaleSetByID)
		s.Require().Equal(enterpriseScaleSetByID.ID, enterpriseScaleSet.ID)
		s.Require().Equal(enterpriseScaleSetByID.Name, enterpriseScaleSet.Name)
	})

	s.T().Run("get scaleset by ID not found", func(_ *testing.T) {
		_, err = s.Store.GetScaleSetByID(s.adminCtx, 999)
		s.Require().Error(err)
		s.Require().Contains(err.Error(), "not found")
	})

	s.T().Run("Set scale set last message ID and desired count", func(_ *testing.T) {
		err = s.Store.SetScaleSetLastMessageID(s.adminCtx, orgScaleSet.ID, 20)
		s.Require().NoError(err)
		err = s.Store.SetScaleSetDesiredRunnerCount(s.adminCtx, orgScaleSet.ID, 5)
		s.Require().NoError(err)
		orgScaleSetByID, err := s.Store.GetScaleSetByID(s.adminCtx, orgScaleSet.ID)
		s.Require().NoError(err)
		s.Require().NotNil(orgScaleSetByID)
		s.Require().Equal(orgScaleSetByID.LastMessageID, int64(20))
		s.Require().Equal(orgScaleSetByID.DesiredRunnerCount, 5)
	})

	updateParams := params.UpdateScaleSetParams{
		Name: "test-scaleset-updated",
		RunnerPrefix: params.RunnerPrefix{
			Prefix: "test-prefix2",
		},
		OSType:            commonParams.Windows,
		OSArch:            commonParams.Arm64,
		ExtraSpecs:        json.RawMessage(`{"test": 111}`),
		Enabled:           garmTesting.Ptr(true),
		MaxRunners:        garmTesting.Ptr(uint(60)),
		MinIdleRunners:    garmTesting.Ptr(uint(50)),
		Image:             "new-test-image",
		Flavor:            "new-test-flavor",
		GitHubRunnerGroup: garmTesting.Ptr("new-test-group"),
	}

	s.T().Run("update repo scaleset", func(_ *testing.T) {
		newRepoScaleSet, err := s.Store.UpdateEntityScaleSet(s.adminCtx, s.repoEntity, repoScaleSet.ID, updateParams, s.callback)
		s.Require().NoError(err)
		s.Require().NotNil(newRepoScaleSet)
		s.Require().NoError(s.callback(repoScaleSet, newRepoScaleSet))
	})

	s.T().Run("update org scaleset", func(_ *testing.T) {
		newOrgScaleSet, err := s.Store.UpdateEntityScaleSet(s.adminCtx, s.orgEntity, orgScaleSet.ID, updateParams, s.callback)
		s.Require().NoError(err)
		s.Require().NotNil(newOrgScaleSet)
		s.Require().NoError(s.callback(orgScaleSet, newOrgScaleSet))
	})

	s.T().Run("update enterprise scaleset", func(_ *testing.T) {
		newEnterpriseScaleSet, err := s.Store.UpdateEntityScaleSet(s.adminCtx, s.enterpriseEntity, enterpriseScaleSet.ID, updateParams, s.callback)
		s.Require().NoError(err)
		s.Require().NotNil(newEnterpriseScaleSet)
		s.Require().NoError(s.callback(enterpriseScaleSet, newEnterpriseScaleSet))
	})

	s.T().Run("update scaleset not found", func(_ *testing.T) {
		_, err = s.Store.UpdateEntityScaleSet(s.adminCtx, s.enterpriseEntity, 99999, updateParams, s.callback)
		s.Require().Error(err)
		s.Require().Contains(err.Error(), "not found")
	})

	s.T().Run("update scaleset with invalid entity", func(_ *testing.T) {
		_, err = s.Store.UpdateEntityScaleSet(s.adminCtx, params.ForgeEntity{}, enterpriseScaleSet.ID, params.UpdateScaleSetParams{}, nil)
		s.Require().Error(err)
		s.Require().Contains(err.Error(), "missing entity id")
	})

	s.T().Run("Create repo scale set instance", func(_ *testing.T) {
		param := params.CreateInstanceParams{
			Name:              "test-instance",
			Status:            commonParams.InstancePendingCreate,
			RunnerStatus:      params.RunnerPending,
			OSType:            commonParams.Linux,
			OSArch:            commonParams.Amd64,
			CallbackURL:       "http://localhost:8080/callback",
			MetadataURL:       "http://localhost:8080/metadata",
			GitHubRunnerGroup: "test-group",
			JitConfiguration: map[string]string{
				"test": "test",
			},
			AgentID: 5,
		}

		instance, err := s.Store.CreateScaleSetInstance(s.adminCtx, repoScaleSet.ID, param)
		s.Require().NoError(err)
		s.Require().NotNil(instance)
		s.Require().Equal(instance.Name, param.Name)
		s.Require().Equal(instance.Status, param.Status)
		s.Require().Equal(instance.RunnerStatus, param.RunnerStatus)
		s.Require().Equal(instance.OSType, param.OSType)
		s.Require().Equal(instance.OSArch, param.OSArch)
		s.Require().Equal(instance.CallbackURL, param.CallbackURL)
		s.Require().Equal(instance.MetadataURL, param.MetadataURL)
		s.Require().Equal(instance.GitHubRunnerGroup, param.GitHubRunnerGroup)
		s.Require().Equal(instance.JitConfiguration, param.JitConfiguration)
		s.Require().Equal(instance.AgentID, param.AgentID)

		s.T().Cleanup(func() {
			err := s.Store.DeleteInstanceByName(s.adminCtx, instance.Name)
			if err != nil {
				s.FailNow(fmt.Sprintf("failed to delete scaleset instance: %s", err))
			}
		})
	})

	s.T().Run("List repo scale set instances", func(_ *testing.T) {
		instances, err := s.Store.ListScaleSetInstances(s.adminCtx, repoScaleSet.ID)
		s.Require().NoError(err)
		s.Require().NotEmpty(instances)
		s.Require().Len(instances, 1)
	})
}

func TestScaleSetsTestSuite(t *testing.T) {
	suite.Run(t, new(ScaleSetsTestSuite))
}
