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

package state

import (
	"testing"

	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
	"gorm.io/gorm"
)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	t.Run("Allows duplicate name in different basis", func(t *testing.T) {
		require, db := RequireAndDB(t)

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

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

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

	t.Run("Allows duplicate path in different basis", func(t *testing.T) {
		require, db := RequireAndDB(t)

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

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

		rid := "RESOURCE ID"
		result := db.Save(
			&Project{
				Name:       "default",
				Path:       "/dev/null",
				ResourceId: rid,
				Basis:      TestBasis(t, db),
			},
		)
		require.NoError(result.Error)
		result = db.Save(
			&Project{
				Name:       "other",
				Path:       "/dev/null/other",
				ResourceId: rid,
				Basis:      TestBasis(t, db),
			},
		)
		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{}
		project := Project{
			Name:        "default",
			Path:        "/dev/null",
			Basis:       TestBasis(t, db),
			Vagrantfile: &vagrantfile,
		}
		result := db.Save(&project)
		require.NoError(result.Error)
		require.NotNil(project.VagrantfileID)
		require.Equal(*project.VagrantfileID, vagrantfile.ID)
	})
}

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

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

		project.Name = ""
		project.Path = ""
		result = db.Save(project)
		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)

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

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

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

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

		project := &Project{
			Name:  "default",
			Path:  "/dev/null",
			Basis: TestBasis(t, db),
		}
		result := db.Save(project)
		require.NoError(result.Error)
		project.Basis = nil
		project.BasisID = 0
		result = db.Save(project)
		require.Error(result.Error)
	})

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

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

		var reloadProject Project
		result = db.First(&reloadProject, &Project{Model: Model{ID: project.ID}})
		require.NoError(result.Error)

		originalResourceId := reloadProject.ResourceId
		reloadProject.ResourceId = "NEW VALUE"
		result = db.Save(&reloadProject)
		require.NoError(result.Error)
		result = db.First(&reloadProject, &Project{Model: Model{ID: project.ID}})
		require.NoError(result.Error)

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

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

		vpath := "/dev/null/Vagrantfile"
		project := Project{
			Name:  "default",
			Path:  "/dev/null",
			Basis: TestBasis(t, db),
		}
		result := db.Save(&project)
		require.NoError(result.Error)
		v := &Vagrantfile{Path: &vpath}
		project.Vagrantfile = v
		result = db.Save(&project)
		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}
		project := Project{
			Name:        "default",
			Path:        "/dev/null",
			Vagrantfile: v,
			Basis:       TestBasis(t, db),
		}
		result := db.Save(&project)
		require.NoError(result.Error)
		require.NotEmpty(v.ID)
		originalID := v.ID

		// Update with new Vagrantfile
		newPath := "/dev/null/new"
		newV := &Vagrantfile{Path: &newPath}
		project.Vagrantfile = newV
		result = db.Save(&project)
		require.NoError(result.Error)
		require.Equal(*project.Vagrantfile.Path, newPath)
		require.Equal(originalID, project.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 TestProject_Delete(t *testing.T) {
	t.Run("Deletes project", func(t *testing.T) {
		require, db := RequireAndDB(t)

		seedProject := TestProject(t, db)

		var project Project
		result := db.First(&project,
			&Project{
				Name: seedProject.Name,
				Path: seedProject.Path,
			},
		)
		require.NoError(result.Error)

		result = db.Where(&Project{ResourceId: project.ResourceId}).
			Delete(&Project{})
		require.NoError(result.Error)
		result = db.First(&Project{}, &Project{ResourceId: project.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(&Project{
			Name:        "default",
			Path:        "/dev/null",
			Basis:       TestBasis(t, db),
			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(&Project{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 targets", func(t *testing.T) {
		require, db := RequireAndDB(t)

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

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

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

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

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

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

		// Set
		_, err := s.ProjectGet(&vagrant_plugin_sdk.Ref_Project{
			ResourceId: "foo",
		})
		require.Error(err)
		require.Equal(codes.NotFound, status.Code(err))
	})

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

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

		// Set
		result, err := s.ProjectPut(TestProject(t, s.db).ToProto())
		require.NoError(err)

		// Get exact
		{
			resp, err := s.ProjectGet(&vagrant_plugin_sdk.Ref_Project{
				ResourceId: result.ResourceId,
			})
			require.NoError(err)
			require.NotNil(resp)
			require.Equal(resp.ResourceId, result.ResourceId)

		}

		// List
		{
			resp, err := s.ProjectList()
			require.NoError(err)
			require.Len(resp, 1)
		}
	})

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

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

		// Set
		result, err := s.ProjectPut(TestProject(t, s.db).ToProto())
		require.NoError(err)

		// Read
		resp, err := s.ProjectGet(&vagrant_plugin_sdk.Ref_Project{
			ResourceId: result.ResourceId,
		})
		require.NoError(err)
		require.NotNil(resp)

		// Delete
		{
			err := s.ProjectDelete(&vagrant_plugin_sdk.Ref_Project{
				ResourceId: result.ResourceId,
			})
			require.NoError(err)
		}

		// Read
		{
			_, err := s.ProjectGet(&vagrant_plugin_sdk.Ref_Project{
				ResourceId: result.ResourceId,
			})
			require.Error(err)
			require.Equal(codes.NotFound, status.Code(err))
		}

		// List
		{
			resp, err := s.ProjectList()
			require.NoError(err)
			require.Len(resp, 0)
		}
	})
}
