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

package state

import (
	"testing"
	"time"

	"github.com/hashicorp/go-memdb"
	"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
	"github.com/stretchr/testify/require"
)

func TestConfig(t *testing.T) {
	t.Run("basic put and get", func(t *testing.T) {
		require := require.New(t)

		s := TestState(t)
		defer s.Close()
		projRef := TestProjectProto(t, s)

		// Create a build
		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Project{
				Project: projRef,
			},

			Name:  "foo",
			Value: "bar",
		}))

		{
			// Get it exactly
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Project{
					Project: projRef,
				},

				Prefix: "foo",
			})
			require.NoError(err)
			require.Len(vs, 1)
		}

		{
			// Get it via a prefix match
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Project{
					Project: projRef,
				},

				Prefix: "",
			})
			require.NoError(err)
			require.Len(vs, 1)
		}

		{
			// non-matching prefix
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Project{
					Project: projRef,
				},

				Prefix: "bar",
			})
			require.NoError(err)
			require.Empty(vs)
		}
	})

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

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

		projRef := TestProjectProto(t, s)

		// Create a build
		require.NoError(s.ConfigSet(
			&vagrant_server.ConfigVar{
				Scope: &vagrant_server.ConfigVar_Project{
					Project: projRef,
				},

				Name:  "global",
				Value: "value",
			},
			&vagrant_server.ConfigVar{
				Scope: &vagrant_server.ConfigVar_Project{
					Project: projRef,
				},

				Name:  "hello",
				Value: "project",
			},
		))

		{
			// Get our merged variables
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Project{
					Project: projRef,
				},
			})
			require.NoError(err)
			require.Len(vs, 2)

			// They are sorted, so check on them
			require.Equal("global", vs[0].Name)
			require.Equal("value", vs[0].Value)
			require.Equal("hello", vs[1].Name)
		}

		{
			// Get project scoped variables. This should return everything.
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Project{
					Project: projRef,
				},
			})
			require.NoError(err)
			require.Len(vs, 2)
		}
	})

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

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

		projRef := TestProjectProto(t, s)

		// Create a var
		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Project{
				Project: projRef,
			},

			Name:  "foo",
			Value: "bar",
		}))

		{
			// Get it exactly
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Project{
					Project: projRef,
				},

				Prefix: "foo",
			})
			require.NoError(err)
			require.Len(vs, 1)
		}

		// Delete it
		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Project{
				Project: projRef,
			},

			Name: "foo",
		}))

		// Should not exist
		{
			// Get it exactly
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Project{
					Project: projRef,
				},

				Prefix: "foo",
			})
			require.NoError(err)
			require.Len(vs, 0)
		}
	})

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

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

		projRef := TestProjectProto(t, s)

		// Create the config
		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Runner{
				Runner: &vagrant_server.Ref_Runner{
					Target: &vagrant_server.Ref_Runner_Any{
						Any: &vagrant_server.Ref_RunnerAny{},
					},
				},
			},

			Name:  "foo",
			Value: "bar",
		}))

		// Create a var that shouldn't match
		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Project{
				Project: projRef,
			},

			Name:  "bar",
			Value: "baz",
		}))

		{
			// Get it exactly.
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Runner{
					Runner: &vagrant_server.Ref_RunnerId{Id: "R_A"},
				},

				Prefix: "foo",
			})
			require.NoError(err)
			require.Len(vs, 1)
		}

		{
			// Get it via a prefix match
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Runner{
					Runner: &vagrant_server.Ref_RunnerId{Id: "R_A"},
				},

				Prefix: "",
			})
			require.NoError(err)
			require.Len(vs, 1)
		}

		{
			// non-matching prefix
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Runner{
					Runner: &vagrant_server.Ref_RunnerId{Id: "R_A"},
				},

				Prefix: "bar",
			})
			require.NoError(err)
			require.Empty(vs)
		}
	})

	t.Run("runner configs targeting ID", func(t *testing.T) {
		require := require.New(t)

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

		projRef := TestProjectProto(t, s)

		// Create the config
		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Runner{
				Runner: &vagrant_server.Ref_Runner{
					Target: &vagrant_server.Ref_Runner_Id{
						Id: &vagrant_server.Ref_RunnerId{
							Id: "R_A",
						},
					},
				},
			},

			Name:  "foo",
			Value: "bar",
		}))

		// Create a var that shouldn't match
		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Project{
				Project: projRef,
			},

			Name:  "bar",
			Value: "baz",
		}))

		{
			// Get it exactly.
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Runner{
					Runner: &vagrant_server.Ref_RunnerId{Id: "R_A"},
				},

				Prefix: "foo",
			})
			require.NoError(err)
			require.Len(vs, 1)
		}

		{
			// Doesn't match
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Runner{
					Runner: &vagrant_server.Ref_RunnerId{Id: "R_B"},
				},

				Prefix: "foo",
			})
			require.NoError(err)
			require.Len(vs, 0)
		}
	})

	t.Run("runner configs targeting any and ID", func(t *testing.T) {
		require := require.New(t)

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

		// Create the config
		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Runner{
				Runner: &vagrant_server.Ref_Runner{
					Target: &vagrant_server.Ref_Runner_Any{
						Any: &vagrant_server.Ref_RunnerAny{},
					},
				},
			},

			Name:  "foo",
			Value: "bar",
		}))

		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Runner{
				Runner: &vagrant_server.Ref_Runner{
					Target: &vagrant_server.Ref_Runner_Id{
						Id: &vagrant_server.Ref_RunnerId{
							Id: "R_A",
						},
					},
				},
			},

			Name:  "foo",
			Value: "baz",
		}))

		{
			// Get it exactly.
			vs, err := s.ConfigGet(&vagrant_server.ConfigGetRequest{
				Scope: &vagrant_server.ConfigGetRequest_Runner{
					Runner: &vagrant_server.Ref_RunnerId{Id: "R_A"},
				},

				Prefix: "foo",
			})
			require.NoError(err)
			require.Len(vs, 1)
			require.Equal("baz", vs[0].Value)
		}
	})
}

func TestConfigWatch(t *testing.T) {
	t.Run("basic put and get", func(t *testing.T) {
		require := require.New(t)

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

		projRef := TestProjectProto(t, s)

		ws := memdb.NewWatchSet()

		// Get it with watch
		vs, err := s.ConfigGetWatch(&vagrant_server.ConfigGetRequest{
			Scope: &vagrant_server.ConfigGetRequest_Project{
				Project: projRef,
			},

			Prefix: "foo",
		}, ws)
		require.NoError(err)
		require.Len(vs, 0)

		// Watch should block
		require.True(ws.Watch(time.After(10 * time.Millisecond)))

		// Create a config
		require.NoError(s.ConfigSet(&vagrant_server.ConfigVar{
			Scope: &vagrant_server.ConfigVar_Project{
				Project: projRef,
			},

			Name:  "foo",
			Value: "bar",
		}))

		require.False(ws.Watch(time.After(100 * time.Millisecond)))
	})
}
