// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package state

import (
	"testing"

	"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
	"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"
)

func TestBasis_Create(t *testing.T) {
	t.Run("Requires name and path", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Basis{})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Name:")
		require.ErrorContains(result.Error, "Path:")
	})

	t.Run("Requires name", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Basis{Path: "/dev/null"})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Name:")
	})

	t.Run("Requires path", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Basis{Name: "default"})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Path:")
	})

	t.Run("Sets resource ID", func(t *testing.T) {
		require, db := RequireAndDB(t)

		basis := Basis{Name: "default", Path: "/dev/null"}
		result := db.Save(&basis)
		require.NoError(result.Error)
		require.NotEmpty(basis.ResourceId)
	})

	t.Run("Retains resource ID", func(t *testing.T) {
		require, db := RequireAndDB(t)

		rid := "RESOURCE_ID"
		basis := Basis{Name: "default", Path: "/dev/null", ResourceId: rid}
		result := db.Save(&basis)
		require.NoError(result.Error)
		require.EqualValues(rid, basis.ResourceId)
	})

	t.Run("Does not allow duplicate name", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Basis{Name: "default", Path: "/dev/null"})
		require.NoError(result.Error)
		result = db.Save(&Basis{Name: "default", Path: "/dev/null/other"})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Name:")
	})

	t.Run("Does not allow duplicate path", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Basis{Name: "default", Path: "/dev/null"})
		require.NoError(result.Error)
		result = db.Save(&Basis{Name: "other", Path: "/dev/null"})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Path:")
	})

	t.Run("Does not allow duplicate resource IDs", func(t *testing.T) {
		require, db := RequireAndDB(t)

		rid := "RESOURCE ID"
		result := db.Save(&Basis{Name: "default", Path: "/dev/null", ResourceId: rid})
		require.NoError(result.Error)
		result = db.Save(&Basis{Name: "other", Path: "/dev/null/other", ResourceId: rid})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "ResourceId:")
	})

	t.Run("Creates Vagrantfile when set", func(t *testing.T) {
		require, db := RequireAndDB(t)

		vagrantfile := Vagrantfile{}
		basis := Basis{
			Name:        "default",
			Path:        "/dev/null",
			Vagrantfile: &vagrantfile,
		}
		result := db.Save(&basis)
		require.NoError(result.Error)
		require.NotNil(basis.VagrantfileID)
		require.Equal(*basis.VagrantfileID, vagrantfile.ID)
	})
}

func TestBasis_Update(t *testing.T) {
	t.Run("Requires name and path", func(t *testing.T) {
		require, db := RequireAndDB(t)

		basis := &Basis{Name: "default", Path: "/dev/null"}
		result := db.Save(basis)
		require.NoError(result.Error)

		basis.Name = ""
		basis.Path = ""
		result = db.Save(basis)
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Name:")
		require.ErrorContains(result.Error, "Path:")
	})

	t.Run("Requires name", func(t *testing.T) {
		require, db := RequireAndDB(t)

		basis := &Basis{Name: "default", Path: "/dev/null"}
		result := db.Save(basis)
		require.NoError(result.Error)
		basis.Name = ""
		result = db.Save(basis)
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Name:")
	})

	t.Run("Requires path", func(t *testing.T) {
		require, db := RequireAndDB(t)

		basis := &Basis{Name: "default", Path: "/dev/null"}
		result := db.Save(basis)
		require.NoError(result.Error)
		basis.Path = ""
		result = db.Save(basis)
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Path:")
	})

	t.Run("Does not update resource ID", func(t *testing.T) {
		require, db := RequireAndDB(t)

		basis := Basis{Name: "default", Path: "/dev/null"}
		result := db.Save(&basis)
		require.NoError(result.Error)
		require.NotEmpty(basis.ResourceId)

		var reloadBasis Basis
		result = db.First(&reloadBasis, &Basis{Model: Model{ID: basis.ID}})
		require.NoError(result.Error)

		originalResourceId := reloadBasis.ResourceId
		reloadBasis.ResourceId = "NEW VALUE"
		result = db.Save(&reloadBasis)
		require.NoError(result.Error)
		result = db.First(&reloadBasis, &Basis{Model: Model{ID: basis.ID}})
		require.NoError(result.Error)

		require.Equal(reloadBasis.ResourceId, originalResourceId)
	})

	t.Run("Adds Vagrantfile", func(t *testing.T) {
		require, db := RequireAndDB(t)

		vpath := "/dev/null/Vagrantfile"
		basis := Basis{Name: "default", Path: "/dev/null"}
		result := db.Save(&basis)
		require.NoError(result.Error)
		v := &Vagrantfile{Path: &vpath}
		basis.Vagrantfile = v
		result = db.Save(&basis)
		require.NoError(result.Error)
		require.NotEmpty(v.ID)
	})

	t.Run("Updates existing Vagrantfile content", func(t *testing.T) {
		require, db := RequireAndDB(t)

		// Create inital basis
		vpath := "/dev/null/Vagrantfile"
		v := &Vagrantfile{Path: &vpath}
		basis := Basis{Name: "default", Path: "/dev/null", Vagrantfile: v}
		result := db.Save(&basis)
		require.NoError(result.Error)
		require.NotEmpty(v.ID)
		originalID := v.ID

		// Update with new Vagrantfile
		newPath := "/dev/null/new"
		newV := &Vagrantfile{Path: &newPath}
		basis.Vagrantfile = newV
		result = db.Save(&basis)
		require.NoError(result.Error)
		require.Equal(*basis.Vagrantfile.Path, newPath)
		require.Equal(originalID, basis.Vagrantfile.ID)

		// Refetch Vagrantfile to ensure persisted changes
		var checkVF Vagrantfile
		result = db.First(&checkVF, &Vagrantfile{Model: Model{ID: originalID}})
		require.NoError(result.Error)
		require.Equal(*checkVF.Path, newPath)

		// Validate only one Vagrantfile has been stored
		var count int64
		result = db.Model(&Vagrantfile{}).Count(&count)
		require.NoError(result.Error)
		require.Equal(int64(1), count)
	})
}

func TestBasis_Delete(t *testing.T) {
	t.Run("Deletes basis", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Basis{Name: "default", Path: "/dev/null"})
		require.NoError(result.Error)

		var basis Basis
		result = db.First(&basis, &Basis{Name: "default"})
		require.NoError(result.Error)

		result = db.Where(&Basis{ResourceId: basis.ResourceId}).
			Delete(&Basis{})
		require.NoError(result.Error)
		result = db.First(&Basis{}, &Basis{ResourceId: basis.ResourceId})
		require.Error(result.Error)
		require.ErrorIs(result.Error, gorm.ErrRecordNotFound)
	})

	t.Run("Deletes Vagrantfile", func(t *testing.T) {
		require, db := RequireAndDB(t)

		vpath := "/dev/null/Vagrantfile"
		result := db.Save(&Basis{
			Name:        "default",
			Path:        "/dev/null",
			Vagrantfile: &Vagrantfile{Path: &vpath},
		})
		require.NoError(result.Error)

		var count int64
		result = db.Model(&Vagrantfile{}).Count(&count)
		require.NoError(result.Error)
		require.Equal(int64(1), count)

		result = db.Where(&Basis{Name: "default"}).
			Delete(&Basis{})
		require.NoError(result.Error)
		result = db.Model((*Vagrantfile)(nil)).Count(&count)
		require.NoError(result.Error)
		require.Equal(int64(0), count)
	})

	t.Run("Deletes Projects", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Basis{
			Name: "default",
			Path: "/dev/null",
			Projects: []*Project{
				{
					Name: "default",
					Path: "/dev/null/default",
				},
				{
					Name: "Other",
					Path: "/dev/null/other",
				},
			},
		})
		require.NoError(result.Error)

		var count int64
		result = db.Model(&Basis{}).Count(&count)
		require.NoError(result.Error)
		require.Equal(int64(1), count)
		result = db.Model(&Project{}).Count(&count)
		require.NoError(result.Error)
		require.Equal(int64(2), count)

		result = db.Where(&Basis{Name: "default"}).
			Delete(&Basis{})
		require.NoError(result.Error)

		result = db.Model(&Basis{}).Count(&count)
		require.NoError(result.Error)
		require.Equal(int64(0), count)
		result = db.Model(&Project{}).Count(&count)
		require.NoError(result.Error)
		require.Equal(int64(0), count)
	})
}

func TestBasis_State(t *testing.T) {
	t.Run("Get returns error if not exist", func(t *testing.T) {
		require := require.New(t)

		s := TestState(t)
		defer s.Close()

		_, err := s.BasisGet(&vagrant_plugin_sdk.Ref_Basis{ResourceId: "nothing"})
		require.Error(err)
	})

	t.Run("Put creates and sets resource ID", func(t *testing.T) {
		require := require.New(t)

		s := TestState(t)
		defer s.Close()

		testBasis := &vagrant_server.Basis{
			Name: "test_name",
			Path: "/User/test/test",
		}

		result, err := s.BasisPut(testBasis)
		require.NoError(err)
		require.NotEmpty(result.ResourceId)
	})

	t.Run("Put fails on duplicate name", func(t *testing.T) {
		require := require.New(t)

		s := TestState(t)
		defer s.Close()

		testBasis := &vagrant_server.Basis{
			Name: "test_name",
			Path: "/User/test/test",
		}

		// Set initial record
		_, err := s.BasisPut(testBasis)
		require.NoError(err)

		// Attempt to set it again
		_, err = s.BasisPut(testBasis)
		require.Error(err)
	})

	t.Run("Put and Get", func(t *testing.T) {
		require := require.New(t)

		s := TestState(t)
		defer s.Close()

		testBasis := &vagrant_server.Basis{
			Name: "test_name",
			Path: "/User/test/test",
		}

		// Set
		result, err := s.BasisPut(testBasis)
		require.NoError(err)

		testBasisRef := &vagrant_plugin_sdk.Ref_Basis{
			ResourceId: result.ResourceId,
		}

		// Get full ref
		resp, err := s.BasisGet(testBasisRef)
		require.NoError(err)
		require.NotNil(resp)
		require.Equal(resp.Name, testBasis.Name)
	})

	t.Run("Find", func(t *testing.T) {
		require := require.New(t)

		s := TestState(t)
		defer s.Close()

		testBasis := &vagrant_server.Basis{
			Name: "test_name",
			Path: "/User/test/test",
		}

		// Set
		result, err := s.BasisPut(testBasis)
		require.NoError(err)

		// Find by resource id
		{
			resp, err := s.BasisFind(&vagrant_server.Basis{
				ResourceId: result.ResourceId,
			})
			require.NoError(err)
			require.NotNil(resp)
			require.Equal(resp.Name, testBasis.Name)
		}

		// Find by name
		{
			resp, err := s.BasisFind(&vagrant_server.Basis{
				Name: "test_name",
			})
			require.NoError(err)
			require.NotNil(resp)
			require.Equal(resp.Name, testBasis.Name)
		}

		// Find by path
		{
			resp, err := s.BasisFind(&vagrant_server.Basis{
				Path: "/User/test/test",
			})
			require.NoError(err)
			require.NotNil(resp)
			require.Equal(resp.Name, testBasis.Name)
		}
	})

	t.Run("Delete", func(t *testing.T) {
		require := require.New(t)

		s := TestState(t)
		defer s.Close()

		testBasis := &vagrant_server.Basis{
			Name: "test_name",
			Path: "/User/test/test",
		}

		testBasisRef := &vagrant_plugin_sdk.Ref_Basis{ResourceId: "test"}

		// Does not throw error if basis does not exist
		err := s.BasisDelete(testBasisRef)
		require.NoError(err)

		// Add basis
		result, err := s.BasisPut(testBasis)
		require.NoError(err)
		testBasisRef.ResourceId = result.ResourceId

		// No error when deleting basis
		err = s.BasisDelete(testBasisRef)
		require.NoError(err)

		// Basis should not exist
		_, err = s.BasisGet(testBasisRef)
		require.Error(err)
	})

	t.Run("List", func(t *testing.T) {
		require := require.New(t)

		s := TestState(t)
		defer s.Close()

		// Add basis'
		_, err := s.BasisPut(&vagrant_server.Basis{
			Name: "test_name",
			Path: "/User/test/test",
		})
		require.NoError(err)

		_, err = s.BasisPut(&vagrant_server.Basis{
			Name: "test_name2",
			Path: "/User/test/test2",
		})
		require.NoError(err)

		b, err := s.BasisList()
		require.NoError(err)
		require.Len(b, 2)
	})
}
