package externaladmin

import (
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func TestDisableAccountRevokesEveryActiveSession(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(externalAccountRows())
	adminMock.ExpectExec("UPDATE `external_admin_accounts` SET .*status.* WHERE `id` = \\?").
		WillReturnResult(sqlmock.NewResult(0, 1))
	// No session id predicate is allowed here: disabling an account must revoke
	// every still-active session for that account, including other devices.
	adminMock.ExpectExec("UPDATE `external_admin_sessions` SET .*revoke_reason.*revoked_at_ms.* WHERE account_id = \\? AND revoked_at_ms = 0").
		WithArgs("account_disabled", sqlmock.AnyArg(), uint64(7)).
		WillReturnResult(sqlmock.NewResult(0, 3))
	adminMock.ExpectCommit()
	expectReloadedAccountAndLinkedUser(adminMock, userMock)

	account, err := service.SetStatus(t.Context(), "fami", 7, "disabled")
	if err != nil {
		t.Fatalf("disable account: %v", err)
	}
	if account.Status != "disabled" {
		t.Fatalf("status = %q", account.Status)
	}
	assertAccountMutationExpectations(t, adminMock, userMock)
}

func TestResetPasswordRevokesEveryActiveSession(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(externalAccountRows())
	adminMock.ExpectExec("UPDATE `external_admin_accounts` SET .*password_change_required.*password_hash.* WHERE `id` = \\?").
		WillReturnResult(sqlmock.NewResult(0, 1))
	// A reset is an administrator credential replacement, so even the currently
	// active browser must re-authenticate with the temporary password.
	adminMock.ExpectExec("UPDATE `external_admin_sessions` SET .*revoke_reason.*revoked_at_ms.* WHERE account_id = \\? AND revoked_at_ms = 0").
		WithArgs("password_reset", sqlmock.AnyArg(), uint64(7)).
		WillReturnResult(sqlmock.NewResult(0, 3))
	adminMock.ExpectCommit()
	expectReloadedAccountAndLinkedUser(adminMock, userMock)

	account, err := service.ResetPassword(t.Context(), "fami", 7, "replacement-password")
	if err != nil {
		t.Fatalf("reset password: %v", err)
	}
	if account.LinkedUser.UserID != 9001 {
		t.Fatalf("linked user = %d", account.LinkedUser.UserID)
	}
	assertAccountMutationExpectations(t, adminMock, userMock)
}

func newAccountMutationFixture(t *testing.T) (*Service, sqlmock.Sqlmock, sqlmock.Sqlmock, func()) {
	t.Helper()
	adminSQL, adminMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create admin sql mock: %v", err)
	}
	adminDB, err := gorm.Open(mysql.New(mysql.Config{Conn: adminSQL, SkipInitializeWithVersion: true}), &gorm.Config{})
	if err != nil {
		adminSQL.Close()
		t.Fatalf("create admin gorm: %v", err)
	}
	userDB, userMock, err := sqlmock.New()
	if err != nil {
		adminSQL.Close()
		t.Fatalf("create user sql mock: %v", err)
	}
	return NewService(adminDB, userDB, Config{}), adminMock, userMock, func() {
		_ = userDB.Close()
		_ = adminSQL.Close()
	}
}

func externalAccountRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"id", "app_code", "linked_app_user_id", "username", "password_hash", "permissions_json", "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", `[]`, "disabled", true, 0, 0, 1, 1, 1)
}

func expectReloadedAccountAndLinkedUser(adminMock sqlmock.Sqlmock, userMock sqlmock.Sqlmock) {
	adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\?").
		WithArgs(uint64(7), uint64(7), 1).WillReturnRows(externalAccountRows())
	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"))
}

func assertAccountMutationExpectations(t *testing.T, adminMock sqlmock.Sqlmock, userMock sqlmock.Sqlmock) {
	t.Helper()
	if err := adminMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("admin sql expectations: %v", err)
	}
	if err := userMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("user sql expectations: %v", err)
	}
}
