package gcp

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"cloud.google.com/go/iam/apiv1/iampb"
	"github.com/stretchr/testify/require"
	iamadmin "google.golang.org/api/iam/v1"
	"google.golang.org/api/option"
)

func TestEnsureProjectCustomRoleReturnsPermissionError(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/v1/projects/project-1/roles/role1", r.URL.Path)
		writeGoogleAPIError(t, w, http.StatusForbidden, "permission denied")
	}))
	defer srv.Close()

	cli := newTestResourceManagerClient(t, srv)
	roleName, err := cli.EnsureProjectCustomRole(context.Background(), "project-1", "role1", testProjectCustomRole())
	require.Empty(t, roleName)
	require.ErrorContains(t, err, "can't get custom role projects/project-1/roles/role1")
}

func TestEnsureProjectCustomRoleHandlesCreateConflict(t *testing.T) {
	var calls []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		calls = append(calls, r.Method+" "+r.URL.Path)
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/v1/projects/project-1/roles/role1" && len(calls) == 1:
			writeGoogleAPIError(t, w, http.StatusNotFound, "not found")
		case r.Method == http.MethodPost && r.URL.Path == "/v1/projects/project-1/roles":
			var req iamadmin.CreateRoleRequest
			require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
			require.Equal(t, "role1", req.RoleId)
			require.Equal(t, []string{"container.clusters.get"}, req.Role.IncludedPermissions)
			writeGoogleAPIError(t, w, http.StatusConflict, "already exists")
		case r.Method == http.MethodGet && r.URL.Path == "/v1/projects/project-1/roles/role1":
			writeJSON(t, w, &iamadmin.Role{
				Name: "projects/project-1/roles/role1",
				Etag: "etag-before-patch",
			})
		case r.Method == http.MethodPatch && r.URL.Path == "/v1/projects/project-1/roles/role1":
			var role iamadmin.Role
			require.NoError(t, json.NewDecoder(r.Body).Decode(&role))
			require.Equal(t, "projects/project-1/roles/role1", role.Name)
			require.Equal(t, "etag-before-patch", role.Etag)
			require.Equal(t, []string{"container.clusters.get"}, role.IncludedPermissions)
			writeJSON(t, w, &role)
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
		}
	}))
	defer srv.Close()

	cli := newTestResourceManagerClient(t, srv)
	roleName, err := cli.EnsureProjectCustomRole(context.Background(), "project-1", "role1", testProjectCustomRole())
	require.NoError(t, err)
	require.Equal(t, "projects/project-1/roles/role1", roleName)
	require.Equal(t, []string{
		"GET /v1/projects/project-1/roles/role1",
		"POST /v1/projects/project-1/roles",
		"GET /v1/projects/project-1/roles/role1",
		"PATCH /v1/projects/project-1/roles/role1",
	}, calls)
}

func TestEnsureProjectCustomRoleSkipsUnchangedPatch(t *testing.T) {
	var calls []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		calls = append(calls, r.Method+" "+r.URL.Path)
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/v1/projects/project-1/roles/role1":
			role := testProjectCustomRole()
			writeJSON(t, w, &iamadmin.Role{
				Name:                "projects/project-1/roles/role1",
				Etag:                "etag-current",
				Title:               role.Title,
				Description:         role.Description,
				IncludedPermissions: role.IncludedPermissions,
				Stage:               "GA",
			})
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
		}
	}))
	defer srv.Close()

	cli := newTestResourceManagerClient(t, srv)
	roleName, err := cli.EnsureProjectCustomRole(context.Background(), "project-1", "role1", testProjectCustomRole())
	require.NoError(t, err)
	require.Equal(t, "projects/project-1/roles/role1", roleName)
	require.Equal(t, []string{"GET /v1/projects/project-1/roles/role1"}, calls)
}

func TestEnsureProjectCustomRoleSkipsUnchangedPatchWhenPermissionsReordered(t *testing.T) {
	role := testProjectCustomRole()
	role.IncludedPermissions = []string{
		"container.clusters.get",
		"gkehub.features.create",
		"gkehub.memberships.get",
	}

	var calls []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		calls = append(calls, r.Method+" "+r.URL.Path)
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/v1/projects/project-1/roles/role1":
			writeJSON(t, w, &iamadmin.Role{
				Name:        "projects/project-1/roles/role1",
				Etag:        "etag-current",
				Title:       role.Title,
				Description: role.Description,
				IncludedPermissions: []string{
					"gkehub.memberships.get",
					"gkehub.features.create",
					"container.clusters.get",
				},
				Stage: "GA",
			})
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
		}
	}))
	defer srv.Close()

	cli := newTestResourceManagerClient(t, srv)
	roleName, err := cli.EnsureProjectCustomRole(context.Background(), "project-1", "role1", role)
	require.NoError(t, err)
	require.Equal(t, "projects/project-1/roles/role1", roleName)
	require.Equal(t, []string{"GET /v1/projects/project-1/roles/role1"}, calls)
}

func TestEnsureProjectCustomRolePreservesExistingExtraPermissions(t *testing.T) {
	var calls []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		calls = append(calls, r.Method+" "+r.URL.Path)
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/v1/projects/project-1/roles/role1":
			role := testProjectCustomRole()
			writeJSON(t, w, &iamadmin.Role{
				Name:        "projects/project-1/roles/role1",
				Etag:        "etag-current",
				Title:       role.Title,
				Description: role.Description,
				IncludedPermissions: []string{
					"container.clusters.get",
					"gkehub.memberships.create",
				},
				Stage: "GA",
			})
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
		}
	}))
	defer srv.Close()

	cli := newTestResourceManagerClient(t, srv)
	roleName, err := cli.EnsureProjectCustomRole(context.Background(), "project-1", "role1", testProjectCustomRole())
	require.NoError(t, err)
	require.Equal(t, "projects/project-1/roles/role1", roleName)
	require.Equal(t, []string{"GET /v1/projects/project-1/roles/role1"}, calls)
}

func TestEnsureProjectCustomRoleAddsMissingPermissionsWithoutDroppingExtraPermissions(t *testing.T) {
	role := testProjectCustomRole()
	role.IncludedPermissions = []string{
		"container.clusters.get",
		"gkehub.features.create",
	}

	var calls []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		calls = append(calls, r.Method+" "+r.URL.Path)
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/v1/projects/project-1/roles/role1":
			writeJSON(t, w, &iamadmin.Role{
				Name:        "projects/project-1/roles/role1",
				Etag:        "etag-current",
				Title:       role.Title,
				Description: role.Description,
				IncludedPermissions: []string{
					"container.clusters.get",
					"gkehub.memberships.create",
				},
				Stage: "GA",
			})
		case r.Method == http.MethodPatch && r.URL.Path == "/v1/projects/project-1/roles/role1":
			var got iamadmin.Role
			require.NoError(t, json.NewDecoder(r.Body).Decode(&got))
			require.Equal(t, "etag-current", got.Etag)
			require.ElementsMatch(t, []string{
				"container.clusters.get",
				"gkehub.features.create",
				"gkehub.memberships.create",
			}, got.IncludedPermissions)
			writeJSON(t, w, &got)
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
		}
	}))
	defer srv.Close()

	cli := newTestResourceManagerClient(t, srv)
	roleName, err := cli.EnsureProjectCustomRole(context.Background(), "project-1", "role1", role)
	require.NoError(t, err)
	require.Equal(t, "projects/project-1/roles/role1", roleName)
	require.Equal(t, []string{
		"GET /v1/projects/project-1/roles/role1",
		"PATCH /v1/projects/project-1/roles/role1",
	}, calls)
}

func TestEnsureProjectCustomRoleUndeletesDeletedRoleBeforePatch(t *testing.T) {
	var getCount int
	var calls []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		calls = append(calls, r.Method+" "+r.URL.Path)
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/v1/projects/project-1/roles/role1":
			getCount++
			if getCount == 1 {
				writeJSON(t, w, &iamadmin.Role{
					Name:    "projects/project-1/roles/role1",
					Etag:    "etag-deleted",
					Deleted: true,
				})
				return
			}
			writeJSON(t, w, &iamadmin.Role{
				Name: "projects/project-1/roles/role1",
				Etag: "etag-after-undelete",
			})
		case r.Method == http.MethodPost && r.URL.Path == "/v1/projects/project-1/roles/role1:undelete":
			writeJSON(t, w, &iamadmin.Role{
				Name: "projects/project-1/roles/role1",
				Etag: "etag-after-undelete",
			})
		case r.Method == http.MethodPatch && r.URL.Path == "/v1/projects/project-1/roles/role1":
			var role iamadmin.Role
			require.NoError(t, json.NewDecoder(r.Body).Decode(&role))
			require.Equal(t, "etag-after-undelete", role.Etag)
			require.False(t, role.Deleted)
			writeJSON(t, w, &role)
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
		}
	}))
	defer srv.Close()

	cli := newTestResourceManagerClient(t, srv)
	roleName, err := cli.EnsureProjectCustomRole(context.Background(), "project-1", "role1", testProjectCustomRole())
	require.NoError(t, err)
	require.Equal(t, "projects/project-1/roles/role1", roleName)
	require.Equal(t, []string{
		"GET /v1/projects/project-1/roles/role1",
		"POST /v1/projects/project-1/roles/role1:undelete",
		"GET /v1/projects/project-1/roles/role1",
		"PATCH /v1/projects/project-1/roles/role1",
	}, calls)
}

func newTestResourceManagerClient(t *testing.T, srv *httptest.Server) *resourceManagerClient {
	t.Helper()

	iamSvc, err := iamadmin.NewService(context.Background(),
		option.WithHTTPClient(srv.Client()),
		option.WithoutAuthentication(),
	)
	require.NoError(t, err)
	iamSvc.BasePath = srv.URL + "/"

	return &resourceManagerClient{
		iamSvc:          iamSvc,
		projectPolicies: make(map[string]*iampb.Policy),
	}
}

func testProjectCustomRole() ProjectCustomRole {
	return ProjectCustomRole{
		Title:               "Test role",
		Description:         "Test role description",
		IncludedPermissions: []string{"container.clusters.get"},
	}
}

func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) {
	t.Helper()

	w.Header().Set("Content-Type", "application/json")
	require.NoError(t, json.NewEncoder(w).Encode(v))
}

func writeGoogleAPIError(t *testing.T, w http.ResponseWriter, code int, message string) {
	t.Helper()

	w.WriteHeader(code)
	writeJSON(t, w, map[string]interface{}{
		"error": map[string]interface{}{
			"code":    code,
			"message": message,
		},
	})
}
