package rbacmanagement_test

import (
	"encoding/json"
	"os"
	"testing"
)

const (
	accountCentralService = "account-central"
	dataServicesService   = "dataservices/central"
	sqlEditorService      = "sqleditor"

	sqlUserResourcePrefix   = "account/central/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sql-user/"
	sqlEditorResourcePrefix = "orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sqleditor/"
)

type privilegeConfig map[string]map[string]struct {
	Description string `json:"description"`
	URL         string `json:"url"`
}

type roleBindingConfig map[string]map[string][]string

func loadJSON[T any](t *testing.T, path string) T {
	t.Helper()

	contents, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read %s: %v", path, err)
	}

	var result T
	if err := json.Unmarshal(contents, &result); err != nil {
		t.Fatalf("decode %s: %v", path, err)
	}
	return result
}

func contains(values []string, wanted string) bool {
	for _, value := range values {
		if value == wanted {
			return true
		}
	}
	return false
}

func TestSQLEditorDefinesTiDBScopedPrivileges(t *testing.T) {
	privileges := loadJSON[privilegeConfig](t, "ALL_PRIVILEGE.json")
	methods := []string{"get", "post", "put", "delete"}

	for _, method := range methods {
		t.Run(method, func(t *testing.T) {
			resource := sqlEditorResourcePrefix + method + "_api"
			privilege, ok := privileges[sqlEditorService][resource]
			if !ok {
				t.Fatalf("missing privilege %q", resource)
			}

			wantURL := method + `:/api/v1/dataapps/sqleditor/orgs/(\d+)/projects/(\d+)/clusters/(\d+)/.*`
			if privilege.URL != wantURL {
				t.Errorf("privilege %q URL = %q, want %q", resource, privilege.URL, wantURL)
			}
		})
	}
}

func TestSQLEditorBindsTiDBScopedPrivilegesToExpectedRoles(t *testing.T) {
	bindings := loadJSON[roleBindingConfig](t, "ALL_ROLEBINDING.json")
	methods := []string{"get", "post", "put", "delete"}
	roles := []string{"org:owner", "project:owner", "project:dev", "project:readonly", "cluster:admin"}

	for _, role := range roles {
		t.Run(role, func(t *testing.T) {
			for _, method := range methods {
				resource := sqlEditorResourcePrefix + method + "_api"
				if !contains(bindings[sqlEditorService][role], resource) {
					t.Errorf("role %q is missing %q", role, resource)
				}
			}
		})
	}
}

func TestSQLEditorBindsGetOnlyToClusterViewer(t *testing.T) {
	bindings := loadJSON[roleBindingConfig](t, "ALL_ROLEBINDING.json")

	getResource := sqlEditorResourcePrefix + "get_api"
	if !contains(bindings[sqlEditorService]["cluster:viewer"], getResource) {
		t.Errorf("cluster:viewer is missing %q", getResource)
	}

	for _, method := range []string{"post", "put", "delete"} {
		resource := sqlEditorResourcePrefix + method + "_api"
		if contains(bindings[sqlEditorService]["cluster:viewer"], resource) {
			t.Errorf("cluster:viewer unexpectedly has %q", resource)
		}
	}
}

func TestSQLUserBindingsMatchProjectRoles(t *testing.T) {
	bindings := loadJSON[roleBindingConfig](t, "ALL_ROLEBINDING.json")

	for _, operation := range []string{"create", "delete", "query", "update"} {
		resource := sqlUserResourcePrefix + operation
		if !contains(bindings[accountCentralService]["project:dev"], resource) {
			t.Errorf("project:dev is missing %q", resource)
		}
	}

	queryResource := sqlUserResourcePrefix + "query"
	if !contains(bindings[accountCentralService]["project:readonly"], queryResource) {
		t.Errorf("project:readonly is missing %q", queryResource)
	}
	for _, operation := range []string{"create", "delete", "update"} {
		resource := sqlUserResourcePrefix + operation
		if contains(bindings[accountCentralService]["project:readonly"], resource) {
			t.Errorf("project:readonly unexpectedly has %q", resource)
		}
	}
}

func TestDataServicesProjectDevCanPost(t *testing.T) {
	bindings := loadJSON[roleBindingConfig](t, "ALL_ROLEBINDING.json")
	resource := "orgs/{org_id}/projects/{project_id}/dataservices/central/post_api"
	if !contains(bindings[dataServicesService]["project:dev"], resource) {
		t.Errorf("project:dev is missing %q", resource)
	}
	if contains(bindings[dataServicesService]["project:readonly"], resource) {
		t.Errorf("project:readonly unexpectedly has %q", resource)
	}
}

func TestFsRegionPrivilegesAndBindings(t *testing.T) {
	const service = "fs"
	privileges := loadJSON[privilegeConfig](t, "ALL_PRIVILEGE.json")
	bindings := loadJSON[roleBindingConfig](t, "ALL_ROLEBINDING.json")

	// fs regions are organization-scoped read-only data served by the mgmt service.
	// The privilege URIs must exactly match the resource URIs produced by
	// tidb-mgmt-portal PathPattern2ResourceMap for the fs OpenAPI paths.
	regionView := []string{
		"/fs/orgs/{org_id}/regions/list",
		"/fs/orgs/{org_id}/regions/get",
	}

	for _, resource := range regionView {
		if _, ok := privileges[service][resource]; !ok {
			t.Errorf("missing privilege %q", resource)
		}
	}

	// The privilege URL must match the fs OpenAPI method and path, like
	// TestSQLEditorDefinesTiDBScopedPrivileges does for sqleditor.
	wantURLs := map[string]string{
		"/fs/orgs/{org_id}/regions/list": "get:/fs/regions",
		"/fs/orgs/{org_id}/regions/get":  "get:/fs/regions/{region_id}",
	}
	for resource, wantURL := range wantURLs {
		privilege, ok := privileges[service][resource]
		if !ok {
			continue
		}
		if privilege.URL != wantURL {
			t.Errorf("privilege %q URL = %q, want %q", resource, privilege.URL, wantURL)
		}
	}

	// Region discovery is needed by every org-level read role, including OpenAPI
	// keys (api:org:owner) and impersonated org owners.
	for _, role := range []string{
		"org:owner",
		"api:org:owner",
		"impersonate:org:owner",
		"org:member",
		"org:billing_admin",
		"org:audit_admin",
		"org:billing_viewer",
	} {
		for _, resource := range regionView {
			if !contains(bindings[service][role], resource) {
				t.Errorf("role %q is missing %q", role, resource)
			}
		}
	}

	// Region data is org-scoped; project/cluster roles must not get it.
	for _, role := range []string{"project:owner", "project:dev", "project:readonly", "cluster:admin", "cluster:viewer"} {
		for _, resource := range regionView {
			if contains(bindings[service][role], resource) {
				t.Errorf("role %q unexpectedly has %q", role, resource)
			}
		}
	}
}

func TestDrive9ObjectStorePrivilegesAndBindings(t *testing.T) {
	const service = "drive9"
	privileges := loadJSON[privilegeConfig](t, "ALL_PRIVILEGE.json")
	bindings := loadJSON[roleBindingConfig](t, "ALL_ROLEBINDING.json")

	// Object backends and namespaces are organization-scoped in Drive9.
	// Do not catalog project-scoped privilege names for them: a project API
	// key would authorize, then the fs handlers would return org-wide data.
	orgViewModify := []string{
		"/drive9/orgs/{org_id}/object-backend/view",
		"/drive9/orgs/{org_id}/object-backend/modify",
		"/drive9/orgs/{org_id}/object-namespace/view",
		"/drive9/orgs/{org_id}/object-namespace/modify",
	}
	orgView := []string{
		"/drive9/orgs/{org_id}/object-backend/view",
		"/drive9/orgs/{org_id}/object-namespace/view",
	}
	projectScoped := []string{
		"/drive9/orgs/{org_id}/projects/{project_id}/object-backend/view",
		"/drive9/orgs/{org_id}/projects/{project_id}/object-backend/modify",
		"/drive9/orgs/{org_id}/projects/{project_id}/object-namespace/view",
		"/drive9/orgs/{org_id}/projects/{project_id}/object-namespace/modify",
	}

	for _, resource := range orgViewModify {
		if _, ok := privileges[service][resource]; !ok {
			t.Errorf("missing privilege %q", resource)
		}
	}
	for _, resource := range projectScoped {
		if _, ok := privileges[service][resource]; ok {
			t.Errorf("unexpected project-scoped privilege %q", resource)
		}
	}

	mustHave := map[string][]string{
		"org:owner":             orgViewModify,
		"impersonate:org:owner": orgView,
	}
	for role, resources := range mustHave {
		for _, resource := range resources {
			if !contains(bindings[service][role], resource) {
				t.Errorf("role %q is missing %q", role, resource)
			}
		}
	}

	allObjectStore := append(append([]string{}, orgViewModify...), projectScoped...)
	for _, role := range []string{"project:owner", "project:dev", "project:readonly", "org:member", "org:audit_admin", "org:billing_admin"} {
		for _, resource := range allObjectStore {
			if contains(bindings[service][role], resource) {
				t.Errorf("role %q unexpectedly has %q", role, resource)
			}
		}
	}
	for _, role := range []string{"org:owner", "impersonate:org:owner"} {
		for _, resource := range projectScoped {
			if contains(bindings[service][role], resource) {
				t.Errorf("role %q unexpectedly has project-scoped %q", role, resource)
			}
		}
	}
}
