package externaladmin

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"reflect"
	"strings"
	"testing"

	"hyapp-admin-server/internal/appctx"
	adminmiddleware "hyapp-admin-server/internal/middleware"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/gin-gonic/gin"
)

func TestPermissionCatalogMatchesCurrentProductCapabilities(t *testing.T) {
	items := PermissionCatalog()
	want := []string{
		"user:list", "user:update", "user-ban:list", "user:ban", "user:unban",
		"host:list", "host:invite", "agency:list", "agency:invite", "bd:list", "bd:invite", "external-super-admin:list",
		"room:list", "room:update", "room:ban", "privilege:list", "privilege:grant", "pretty-id:grant",
		"user-title:grant", "room-background:grant", "user-level:grant", "banner:create",
		"banner:update",
	}
	if len(items) != len(want) {
		t.Fatalf("catalog count = %d, want %d", len(items), len(want))
	}
	for index, item := range items {
		if item.Code != want[index] || item.Label == "" || item.Group == "" || !item.DefaultGranted {
			t.Fatalf("catalog[%d] = %+v, want code %s with display metadata/default", index, item, want[index])
		}
		if !reflect.DeepEqual(item.Capabilities, []string{item.Code}) {
			t.Fatalf("catalog capability must be independently addressable: %+v", item)
		}
	}
}

func TestPermissionSelectionAllowlistDependenciesAndNormalization(t *testing.T) {
	if got, err := validatePermissionSelection(nil); err != nil || len(got) != 0 {
		t.Fatalf("explicit empty permissions must be valid: got=%v err=%v", got, err)
	}
	all := append(DefaultPermissionSnapshot(), " user:list ", "user:list")
	got, err := validatePermissionSelection(all)
	if err != nil {
		t.Fatalf("default permission validation: %v", err)
	}
	if !reflect.DeepEqual(got, DefaultPermissionSnapshot()) {
		t.Fatalf("permissions not deduplicated/sorted: got=%v", got)
	}
	for _, invalid := range [][]string{
		{"unknown:permission"},
		{"*"},
		{"external-admin:*"},
		{"privilege:grant"},
		{"user-title:grant", "room-background:grant"},
	} {
		if _, err := validatePermissionSelection(invalid); !errors.Is(err, ErrInvalidPermissions) {
			t.Fatalf("invalid selection %v error = %v", invalid, err)
		}
	}
}

func TestPermissionValidationMessagesUseCatalogLabelsWithoutReflectingCodes(t *testing.T) {
	_, dependencyErr := validatePermissionSelection([]string{"privilege:grant"})
	dependencyMessage := permissionValidationMessage(dependencyErr)
	if !strings.Contains(dependencyMessage, "特权道具发放") || !strings.Contains(dependencyMessage, "用户称号下发") || strings.Contains(dependencyMessage, "privilege:grant") {
		t.Fatalf("dependency message leaked internal code: %q", dependencyMessage)
	}
	_, unknownErr := validatePermissionSelection([]string{"attacker:controlled"})
	unknownMessage := permissionValidationMessage(unknownErr)
	if unknownMessage != "外管权限中包含不支持的权限" || strings.Contains(unknownMessage, "attacker") {
		t.Fatalf("unknown message reflected input: %q", unknownMessage)
	}
}

func TestEffectivePermissionsSafelyUpgradesLegacyAndRejectsCorruptSnapshots(t *testing.T) {
	legacy := []string{
		"app-user:view", "app-user:update", "app-user:status", "app-user:level",
		"host:view", "agency:view", "bd:view", "room:view", "room:update",
		"resource:view", "resource-grant:view", "resource-grant:create",
		"pretty-id:view", "pretty-id:grant", "app-config:view", "app-config:update",
		"vip-config:grant", "upload:create",
	}
	// Retired hierarchy/menu permissions and newly introduced write powers are not
	// inferred from an old snapshot. A platform administrator must delegate them.
	wantLegacy := []string{
		"agency:list", "banner:create", "bd:list", "host:list", "pretty-id:grant",
		"privilege:grant", "privilege:list", "room-background:grant", "room:list", "room:update",
		"user-ban:list", "user-level:grant", "user-title:grant", "user:ban", "user:list",
		"user:unban", "user:update",
	}
	if got := effectivePermissions(legacy); !reflect.DeepEqual(got, wantLegacy) {
		t.Fatalf("legacy default expansion = %v, want fail-closed %v", got, wantLegacy)
	}
	got := effectivePermissions([]string{"user:list", "privilege:grant", "unknown:permission", "*", "external-admin:*"})
	if !reflect.DeepEqual(got, []string{"user:list"}) {
		t.Fatalf("corrupt new snapshot must remove incomplete bundle/unknowns: %v", got)
	}
	got = effectivePermissions([]string{"app-config:view", "app-config:update", "app-user:level", "resource:view"})
	if len(got) != 0 {
		t.Fatalf("incomplete legacy closures must fail closed: %v", got)
	}
}

func TestCreatePermissionFieldDistinguishesOmittedEmptyAndNull(t *testing.T) {
	var omitted createAccountRequest
	if err := json.Unmarshal([]byte(`{"targetUserId":"1","username":"operator","password":"password"}`), &omitted); err != nil {
		t.Fatalf("decode omitted: %v", err)
	}
	if omitted.Permissions.Set {
		t.Fatal("omitted permissions must keep legacy default")
	}
	var empty createAccountRequest
	if err := json.Unmarshal([]byte(`{"targetUserId":"1","username":"operator","password":"password","permissions":[]}`), &empty); err != nil {
		t.Fatalf("decode empty: %v", err)
	}
	if !empty.Permissions.Set || len(empty.Permissions.Value) != 0 {
		t.Fatalf("explicit [] was not preserved: %+v", empty.Permissions)
	}
	var nullValue createAccountRequest
	if err := json.Unmarshal([]byte(`{"targetUserId":"1","username":"operator","password":"password","permissions":null}`), &nullValue); err == nil {
		t.Fatal("permissions:null must be rejected as ambiguous")
	}
}

func TestCreatePermissionSnapshotKeepsOmittedAndExplicitEmptyDistinct(t *testing.T) {
	omitted, err := createPermissionSnapshot(CreateInput{})
	if err != nil || !reflect.DeepEqual(omitted, DefaultPermissionSnapshot()) {
		t.Fatalf("omitted snapshot=%v err=%v", omitted, err)
	}
	empty, err := createPermissionSnapshot(CreateInput{PermissionsSet: true, Permissions: []string{}})
	if err != nil || len(empty) != 0 {
		t.Fatalf("explicit empty snapshot=%v err=%v", empty, err)
	}
	if _, err := createPermissionSnapshot(CreateInput{PermissionsSet: true, Permissions: []string{"*"}}); !errors.Is(err, ErrInvalidPermissions) {
		t.Fatalf("explicit wildcard error=%v", err)
	}
}

func TestCreateAccountRequiresCredentialAndPermissionDelegationCapabilities(t *testing.T) {
	gin.SetMode(gin.TestMode)
	tests := []struct {
		name        string
		permissions []string
		wantStatus  int
	}{
		{name: "create alone cannot delegate default full snapshot", permissions: []string{"external-admin-user:create"}, wantStatus: http.StatusForbidden},
		{name: "delegation alone cannot create credential", permissions: []string{"external-admin-user:permissions"}, wantStatus: http.StatusForbidden},
		{name: "both capabilities reach request validation", permissions: []string{"external-admin-user:create", "external-admin-user:permissions"}, wantStatus: http.StatusBadRequest},
	}
	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			engine := gin.New()
			engine.Use(func(c *gin.Context) {
				c.Set(adminmiddleware.ContextPermissions, testCase.permissions)
				c.Next()
			})
			RegisterAdminRoutes(engine.Group(""), &Handler{service: NewService(nil, nil, Config{})})
			request := httptest.NewRequest(http.MethodPost, "/admin/external-admin-users", strings.NewReader(`{}`))
			request.Header.Set("Content-Type", "application/json")
			recorder := httptest.NewRecorder()
			engine.ServeHTTP(recorder, request)
			if recorder.Code != testCase.wantStatus {
				t.Fatalf("permissions=%v status=%d body=%s", testCase.permissions, recorder.Code, recorder.Body.String())
			}
		})
	}

	// Handler-level defense must also reject omitted permissions when a caller
	// bypasses the standard route middleware during a future refactor.
	engine := gin.New()
	engine.POST("/direct", (&Handler{service: NewService(nil, nil, Config{})}).CreateAccount)
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/direct", strings.NewReader(`{"targetUserId":"1","username":"operator","password":"password1"}`))
	request.Header.Set("Content-Type", "application/json")
	engine.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusForbidden {
		t.Fatalf("direct omitted-permission create status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestGetAccountPermissionsIsScopedToAppAndReturnsRevision(t *testing.T) {
	service, adminMock, userMock, cleanup := newAccountMutationFixture(t)
	defer cleanup()
	adminMock.ExpectQuery("SELECT `id`,`permissions_json`,`permission_revision` FROM `external_admin_accounts` WHERE id = \\? AND app_code = \\? LIMIT \\?").
		WithArgs(uint64(7), "fami", 1).
		WillReturnRows(sqlmock.NewRows([]string{"id", "permissions_json", "permission_revision"}).AddRow(7, `["user:update","user:list"]`, 9))

	view, err := service.GetAccountPermissions(t.Context(), " FAMI ", 7)
	if err != nil {
		t.Fatalf("get permissions: %v", err)
	}
	if view.AccountID != 7 || view.Revision != 9 || !reflect.DeepEqual(view.Permissions, []string{"user:list", "user:update"}) {
		t.Fatalf("permission view=%+v", view)
	}
	assertAccountMutationExpectations(t, adminMock, userMock)
}

func TestUpdatePermissionsUsesRevisionAndRevokesSessionsAtomically(t *testing.T) {
	service, adminMock, userMock, cleanup := newAccountMutationFixture(t)
	defer cleanup()
	adminMock.ExpectBegin()
	adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\? AND app_code = \\?").
		WithArgs(uint64(7), "fami", 1).WillReturnRows(permissionAccountRows(4, `["user:list"]`))
	adminMock.ExpectExec("UPDATE `external_admin_accounts` SET .*permission_revision.*permissions_json.* WHERE `id` = \\?").
		WillReturnResult(sqlmock.NewResult(0, 1))
	adminMock.ExpectExec("UPDATE `external_admin_sessions` SET .*revoke_reason.*revoked_at_ms.* WHERE account_id = \\? AND revoked_at_ms = 0").
		WithArgs("permissions_changed", sqlmock.AnyArg(), uint64(7)).WillReturnResult(sqlmock.NewResult(0, 2))
	adminMock.ExpectCommit()
	adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\? AND app_code = \\?").
		WithArgs(uint64(7), "fami", 1).WillReturnRows(permissionAccountRows(5, `["user:list","user:update"]`))
	expectPermissionLinkedUser(userMock)

	result, err := service.UpdateAccountPermissions(t.Context(), "fami", 7, UpdatePermissionsInput{
		Permissions: []string{"user:update", "user:list", "user:update"}, ExpectedRevision: 4,
	})
	if err != nil {
		t.Fatalf("update permissions: %v", err)
	}
	if !result.Changed || !result.SessionsRevoked || result.Account.PermissionRevision != 5 || !reflect.DeepEqual(result.PreviousPermissions, []string{"user:list"}) {
		t.Fatalf("unexpected update result: %+v", result)
	}
	assertAccountMutationExpectations(t, adminMock, userMock)
}

func TestUpdatePermissionsRejectsStaleRevisionWithoutWriting(t *testing.T) {
	service, adminMock, userMock, cleanup := newAccountMutationFixture(t)
	defer cleanup()
	adminMock.ExpectBegin()
	adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\? AND app_code = \\?").
		WithArgs(uint64(7), "fami", 1).WillReturnRows(permissionAccountRows(5, `["user:list"]`))
	adminMock.ExpectRollback()

	_, err := service.UpdateAccountPermissions(t.Context(), "fami", 7, UpdatePermissionsInput{
		Permissions: []string{"user:list", "user:update"}, ExpectedRevision: 4,
	})
	if !errors.Is(err, ErrPermissionConflict) {
		t.Fatalf("stale revision error = %v", err)
	}
	assertAccountMutationExpectations(t, adminMock, userMock)
}

func TestUpdatePermissionsNoopIsIdempotentWithoutRevisionOrSessionChange(t *testing.T) {
	service, adminMock, userMock, cleanup := newAccountMutationFixture(t)
	defer cleanup()
	adminMock.ExpectBegin()
	adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\? AND app_code = \\?").
		WithArgs(uint64(7), "fami", 1).WillReturnRows(permissionAccountRows(6, `["user:list"]`))
	adminMock.ExpectCommit()
	adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\? AND app_code = \\?").
		WithArgs(uint64(7), "fami", 1).WillReturnRows(permissionAccountRows(6, `["user:list"]`))
	expectPermissionLinkedUser(userMock)

	result, err := service.UpdateAccountPermissions(t.Context(), "fami", 7, UpdatePermissionsInput{
		Permissions: []string{" user:list ", "user:list"}, ExpectedRevision: 6,
	})
	if err != nil || result.Changed || result.SessionsRevoked || result.Account.PermissionRevision != 6 {
		t.Fatalf("noop result=%+v err=%v", result, err)
	}
	assertAccountMutationExpectations(t, adminMock, userMock)
}

func TestUpdatePermissionsHandlerMapsRevisionConflictTo409(t *testing.T) {
	service, adminMock, userMock, cleanup := newAccountMutationFixture(t)
	defer cleanup()
	adminMock.ExpectBegin()
	adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\? AND app_code = \\?").
		WithArgs(uint64(7), "fami", 1).WillReturnRows(permissionAccountRows(3, `["user:list"]`))
	adminMock.ExpectRollback()

	gin.SetMode(gin.TestMode)
	engine := gin.New()
	engine.PUT("/accounts/:id", func(c *gin.Context) {
		c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "fami"))
		c.Next()
	}, (&Handler{service: service}).UpdateAccountPermissions)
	request := httptest.NewRequest(http.MethodPut, "/accounts/7", strings.NewReader(`{"permissions":["user:list"],"expectedRevision":2}`))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()
	engine.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusConflict {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	assertAccountMutationExpectations(t, adminMock, userMock)
}

func permissionAccountRows(revision uint64, permissions string) *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"id", "app_code", "linked_app_user_id", "username", "password_hash", "permissions_json", "permission_revision", "status",
		"password_change_required", "failed_login_count", "locked_until_ms", "created_by_admin_id", "created_at_ms", "updated_at_ms",
	}).AddRow(7, "fami", 9001, "operator", "hash", permissions, revision, "active", false, 0, 0, 1, 1, 1)
}

func expectPermissionLinkedUser(userMock sqlmock.Sqlmock) {
	userMock.ExpectQuery("SELECT u.user_id,").
		WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), "fami", int64(9001)).
		WillReturnRows(sqlmock.NewRows([]string{
			"user_id", "current_display_user_id", "default_display_user_id", "pretty_display_user_id", "pretty_id", "username", "avatar", "status",
		}).AddRow(9001, "123456", "654321", "8888", "8888", "linked-user", "avatar", "active"))
}
