// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package agent

import (
	"bytes"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/hashicorp/nomad/api"
	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/nomad/state"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/stretchr/testify/require"
)

func TestHTTP_CSIEndpointPlugin(t *testing.T) {
	ci.Parallel(t)
	httpTest(t, nil, func(s *TestAgent) {
		server := s.Agent.Server()
		cleanup := state.CreateTestCSIPlugin(server.State(), "foo")
		defer cleanup()

		body := bytes.NewBuffer(nil)
		req, err := http.NewRequest(http.MethodGet, "/v1/plugin/csi/foo", body)
		require.NoError(t, err)

		resp := httptest.NewRecorder()
		obj, err := s.Server.CSIPluginSpecificRequest(resp, req)
		require.NoError(t, err)
		require.Equal(t, 200, resp.Code)

		out, ok := obj.(*structs.CSIPlugin)
		require.True(t, ok)

		// ControllersExpected is 0 because this plugin was created without a job,
		// which sets expected
		require.Equal(t, 0, out.ControllersExpected)
		require.Equal(t, 1, out.ControllersHealthy)
		require.Len(t, out.Controllers, 1)

		require.Equal(t, 0, out.NodesExpected)
		require.Equal(t, 2, out.NodesHealthy)
		require.Len(t, out.Nodes, 2)
	})
}

func TestHTTP_CSIParseSecrets(t *testing.T) {
	ci.Parallel(t)
	testCases := []struct {
		val    string
		expect structs.CSISecrets
	}{
		{"", nil},
		{"one", nil},
		{"one,two", nil},
		{"one,two=value_two",
			structs.CSISecrets(map[string]string{"two": "value_two"})},
		{"one=value_one,one=overwrite",
			structs.CSISecrets(map[string]string{"one": "overwrite"})},
		{"one=value_one,two=value_two",
			structs.CSISecrets(map[string]string{"one": "value_one", "two": "value_two"})},
		{"one=value_one=two,two=value_two",
			structs.CSISecrets(map[string]string{"one": "value_one=two", "two": "value_two"})},
	}
	for _, tc := range testCases {
		req, _ := http.NewRequest(http.MethodGet, "/v1/plugin/csi/foo", nil)
		req.Header.Add("X-Nomad-CSI-Secrets", tc.val)
		require.Equal(t, tc.expect, parseCSISecrets(req), tc.val)
	}
}

func TestHTTP_CSIEndpointRegisterVolume(t *testing.T) {
	ci.Parallel(t)
	httpTest(t, nil, func(s *TestAgent) {
		server := s.Agent.Server()
		cleanup := state.CreateTestCSIPluginNodeOnly(server.State(), "foo")
		defer cleanup()

		args := structs.CSIVolumeRegisterRequest{
			Volumes: []*structs.CSIVolume{{
				ID:       "bar",
				PluginID: "foo",
				RequestedCapabilities: []*structs.CSIVolumeCapability{{
					AccessMode:     structs.CSIVolumeAccessModeMultiNodeSingleWriter,
					AttachmentMode: structs.CSIVolumeAttachmentModeFilesystem,
				}},
			}},
		}
		body := encodeReq(args)
		req, err := http.NewRequest(http.MethodPut, "/v1/volumes", body)
		require.NoError(t, err)
		resp := httptest.NewRecorder()
		_, err = s.Server.CSIVolumesRequest(resp, req)
		require.NoError(t, err, "put error")

		req, err = http.NewRequest(http.MethodGet, "/v1/volume/csi/bar", nil)
		require.NoError(t, err)
		resp = httptest.NewRecorder()
		raw, err := s.Server.CSIVolumeSpecificRequest(resp, req)
		require.NoError(t, err, "get error")
		out, ok := raw.(*structs.CSIVolume)
		require.True(t, ok)
		require.Equal(t, 1, out.ControllersHealthy)
		require.Equal(t, 2, out.NodesHealthy)

		req, err = http.NewRequest(http.MethodDelete, "/v1/volume/csi/bar/detach", nil)
		require.NoError(t, err)
		resp = httptest.NewRecorder()
		_, err = s.Server.CSIVolumeSpecificRequest(resp, req)
		require.Equal(t, CodedError(400, "detach requires node ID"), err)
	})
}

func TestHTTP_CSIEndpointCreateVolume(t *testing.T) {
	ci.Parallel(t)
	httpTest(t, nil, func(s *TestAgent) {
		server := s.Agent.Server()
		cleanup := state.CreateTestCSIPlugin(server.State(), "foo")
		defer cleanup()

		args := structs.CSIVolumeCreateRequest{
			Volumes: []*structs.CSIVolume{{
				ID:       "baz",
				PluginID: "foo",
				RequestedCapabilities: []*structs.CSIVolumeCapability{{
					AccessMode:     structs.CSIVolumeAccessModeMultiNodeSingleWriter,
					AttachmentMode: structs.CSIVolumeAttachmentModeFilesystem,
				}},
			}},
		}
		body := encodeReq(args)
		req, err := http.NewRequest(http.MethodPut, "/v1/volumes/create", body)
		require.NoError(t, err)
		resp := httptest.NewRecorder()
		_, err = s.Server.CSIVolumesRequest(resp, req)
		require.Error(t, err, "controller validate volume: No path to node")

		req, err = http.NewRequest(http.MethodDelete, "/v1/volume/csi/baz", nil)
		require.NoError(t, err)
		resp = httptest.NewRecorder()
		_, err = s.Server.CSIVolumeSpecificRequest(resp, req)
		require.Error(t, err, "volume not found: baz")
	})
}

func TestHTTP_CSIEndpointSnapshot(t *testing.T) {
	ci.Parallel(t)
	httpTest(t, nil, func(s *TestAgent) {
		server := s.Agent.Server()
		cleanup := state.CreateTestCSIPlugin(server.State(), "foo")
		defer cleanup()

		args := &api.CSISnapshotCreateRequest{
			Snapshots: []*api.CSISnapshot{{
				Name:           "snap-*",
				PluginID:       "foo",
				SourceVolumeID: "bar",
			}},
		}
		body := encodeReq(args)
		req, err := http.NewRequest(http.MethodPut, "/v1/volumes/snapshot", body)
		require.NoError(t, err)
		resp := httptest.NewRecorder()
		_, err = s.Server.CSISnapshotsRequest(resp, req)
		require.Error(t, err, "no such volume: bar")
	})
}
