// 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"
)

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

		result := db.Save(&Box{})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Directory:")
		require.ErrorContains(result.Error, "Name:")
		require.ErrorContains(result.Error, "Provider:")
	})

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

		result := db.Save(&Box{Name: "default", Provider: "virt"})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Directory:")
	})

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

		result := db.Save(&Box{Provider: "virt", Directory: "/dev/null"})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Name:")
	})

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

		result := db.Save(&Box{Name: "default", Directory: "/dev/null"})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Provider:")
	})

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

		result := db.Save(&Box{
			Name:      "default",
			Directory: "/dev/null",
			Provider:  "virt",
		})
		require.NoError(result.Error)
		var box Box
		result = db.First(&box, &Box{Name: "default"})
		require.NoError(result.Error)
		require.NotEmpty(box.ResourceId)
	})

	t.Run("Defaults version when not set", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Box{
			Name:      "default",
			Directory: "/dev/null",
			Provider:  "virt",
		})
		require.NoError(result.Error)
		var box Box
		result = db.First(&box, &Box{Name: "default"})
		require.NoError(result.Error)
		require.Equal(DEFAULT_BOX_VERSION, box.Version)
	})

	t.Run("Defaults version when set to 0", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Box{
			Name:      "default",
			Directory: "/dev/null",
			Provider:  "virt",
			Version:   "0",
		})
		require.NoError(result.Error)
		var box Box
		result = db.First(&box, &Box{Name: "default"})
		require.NoError(result.Error)
		require.Equal(DEFAULT_BOX_VERSION, box.Version)
	})

	t.Run("Requires version to be semver", func(t *testing.T) {
		require, db := RequireAndDB(t)

		box := &Box{
			Name:      "default",
			Directory: "/dev/null",
			Provider:  "virt",
			Version:   "0.a",
		}

		result := db.Save(box)
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Version:")

		box.Version = "string"
		result = db.Save(box)
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Version:")

		box.Version = "a0.1.2"
		result = db.Save(box)
		require.Error(result.Error)
		require.ErrorContains(result.Error, "Version:")
	})

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

		result := db.Save(&Box{
			Name:      "default",
			Directory: "/dev/null",
			Provider:  "virt",
			Version:   "1.0.0",
		})
		require.NoError(result.Error)

		result = db.Save(&Box{
			Name:      "default",
			Directory: "/dev/null/other",
			Provider:  "virt",
			Version:   "1.0.0",
		})
		require.Error(result.Error)
		require.ErrorContains(result.Error, "name")
		require.ErrorContains(result.Error, "provider")
		require.ErrorContains(result.Error, "version")
	})

	t.Run("Allows multiple versions", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Box{
			Name:      "default",
			Directory: "/dev/null",
			Provider:  "virt",
			Version:   "1.0.0",
		})
		require.NoError(result.Error)

		result = db.Save(&Box{
			Name:      "default",
			Directory: "/dev/null/other",
			Provider:  "virt",
			Version:   "1.0.1",
		})
		require.NoError(result.Error)
	})

	t.Run("Allows multiple providers", func(t *testing.T) {
		require, db := RequireAndDB(t)

		result := db.Save(&Box{
			Name:      "default",
			Directory: "/dev/null",
			Provider:  "virt",
			Version:   "1.0.0",
		})
		require.NoError(result.Error)

		result = db.Save(&Box{
			Name:      "default",
			Directory: "/dev/null/other",
			Provider:  "virtz",
			Version:   "1.0.0",
		})
		require.NoError(result.Error)
	})
}

func TestBox_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.BoxGet(&vagrant_plugin_sdk.Ref_Box{ResourceId: "nothing"})
		require.Error(err)
	})

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

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

		testBox := &vagrant_server.Box{
			ResourceId: "qwerwasdf",
			Directory:  "/directory",
			Name:       "hashicorp/bionic",
			Version:    "1.2.3",
			Provider:   "virtualbox",
		}

		testBox2 := &vagrant_server.Box{
			ResourceId: "qwerwasdf-2",
			Directory:  "/directory-2",
			Name:       "hashicorp/bionic",
			Version:    "1.2.4",
			Provider:   "virtualbox",
		}

		testBoxRef := &vagrant_plugin_sdk.Ref_Box{
			ResourceId: "qwerwasdf",
			Name:       "hashicorp/bionic",
			Version:    "1.2.3",
			Provider:   "virtualbox",
		}

		testBoxRef2 := &vagrant_plugin_sdk.Ref_Box{
			ResourceId: "qwerwasdf-2",
			Name:       "hashicorp/bionic",
			Version:    "1.2.4",
			Provider:   "virtualbox",
		}

		// Set
		err := s.BoxPut(testBox)
		require.NoError(err)
		err = s.BoxPut(testBox2)
		require.NoError(err)

		// Get full ref
		{
			resp, err := s.BoxGet(testBoxRef)
			require.NoError(err)
			require.NotNil(resp)
			require.Equal(resp.Name, testBox.Name)

			resp, err = s.BoxGet(testBoxRef2)
			require.NoError(err)
			require.NotNil(resp)
			require.Equal(resp.Name, testBox2.Name)
		}

		// Get by id
		{
			resp, err := s.BoxGet(&vagrant_plugin_sdk.Ref_Box{
				ResourceId: "qwerwasdf",
			})
			require.NoError(err)
			require.NotNil(resp)
			require.Equal(resp.Name, testBox.Name)
		}
	})

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

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

		testBox := &vagrant_server.Box{
			ResourceId: "qwerwasdf",
			Directory:  "/directory",
			Name:       "hashicorp/bionic",
			Version:    "1.2.3",
			Provider:   "virtualbox",
		}

		testBoxRef := &vagrant_plugin_sdk.Ref_Box{
			ResourceId: "qwerwasdf",
		}
		err := s.BoxDelete(testBoxRef)
		require.NoError(err)

		err = s.BoxPut(testBox)
		require.NoError(err)

		err = s.BoxDelete(testBoxRef)
		require.NoError(err)

		_, err = s.BoxGet(testBoxRef)
		require.Error(err)
	})

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

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

		err := s.BoxPut(&vagrant_server.Box{
			ResourceId: "qwerwasdf",
			Directory:  "/directory",
			Name:       "hashicorp/bionic",
			Version:    "1.2.3",
			Provider:   "virtualbox",
		})
		require.NoError(err)

		err = s.BoxPut(&vagrant_server.Box{
			ResourceId: "rrbrwasdf",
			Directory:  "/other-directory",
			Name:       "hashicorp/bionic",
			Version:    "1.2.4",
			Provider:   "virtualbox",
		})
		require.NoError(err)

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

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

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

		err := s.BoxPut(&vagrant_server.Box{
			ResourceId: "hashicorp/bionic-1.2.3-virtualbox",
			Directory:  "/directory",
			Name:       "hashicorp/bionic",
			Version:    "1.2.3",
			Provider:   "virtualbox",
		})
		require.NoError(err)

		err = s.BoxPut(&vagrant_server.Box{
			ResourceId: "hashicorp/bionic-1.2.4-virtualbox",
			Directory:  "/other-directory",
			Name:       "hashicorp/bionic",
			Version:    "1.2.4",
			Provider:   "virtualbox",
		})
		require.NoError(err)

		err = s.BoxPut(&vagrant_server.Box{
			ResourceId: "box-0-virtualbox",
			Directory:  "/another-directory",
			Name:       "box",
			Version:    "0",
			Provider:   "virtualbox",
		})
		require.NoError(err)

		b, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Name:     "hashicorp/bionic",
			Provider: "virtualbox",
		})
		require.NoError(err)
		require.Equal(b.Name, "hashicorp/bionic")
		require.Equal(b.Version, "1.2.4")

		b3, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Name:     "hashicorp/bionic",
			Version:  "1.2.3",
			Provider: "virtualbox",
		})
		require.NoError(err)
		require.Equal(b3.Name, "hashicorp/bionic")
		require.Equal(b3.Version, "1.2.3")
		require.Equal(b3.Provider, "virtualbox")

		b4, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Name:     "hashicorp/bionic",
			Version:  "1.2.3",
			Provider: "dontexist",
		})
		require.NoError(err)
		require.Nil(b4)

		b5, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Name:     "hashicorp/bionic",
			Version:  "9.9.9",
			Provider: "virtualbox",
		})
		require.NoError(err)
		require.Nil(b5)

		b6, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Version: "1.2.3",
		})
		require.Error(err)
		require.Nil(b6)

		b7, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Name: "dontexist",
		})
		require.NoError(err)
		require.Nil(b7)

		b8, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Name:     "hashicorp/bionic",
			Provider: "virtualbox",
			Version:  "~> 1.2",
		})
		require.NoError(err)
		require.Equal(b8.Name, "hashicorp/bionic")
		require.Equal(b8.Version, "1.2.4")

		b9, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Name:     "hashicorp/bionic",
			Provider: "virtualbox",
			Version:  "> 1.0, < 3.0",
		})
		require.NoError(err)
		require.Equal(b9.Name, "hashicorp/bionic")
		require.Equal(b9.Version, "1.2.4")

		b10, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Name:    "hashicorp/bionic",
			Version: "< 1.0",
		})
		require.NoError(err)
		require.Nil(b10)

		b11, err := s.BoxFind(&vagrant_plugin_sdk.Ref_Box{
			Name:     "box",
			Version:  "0",
			Provider: "virtualbox",
		})
		require.NoError(err)
		require.Equal(b11.Name, "box")
		require.Equal(b11.Version, "0.0.0")
	})
}
