================================================================================
           DANH SÁCH 49 KỊCH BẢN KIỂM THỬ - LARAVEL DUSK BROWSER TESTS
           Dự án: Quay Số May Mắn - quayso.sisvietnam.vn
           Tổng kết: 49 tests PASSED | 90 assertions | ~103 giây
================================================================================

--------------------------------------------------------------------------------
FILE 1: tests/Browser/ExampleTest.php  (1 test)
--------------------------------------------------------------------------------

[TC-01] test_basic_example
  Mô tả  : Kiểm tra cơ bản trang chủ tải được vòng quay may mắn.
  Bước   : 1. Truy cập URL gốc "/".
  Kết quả: Phần tử #wheelCanvas (canvas vòng quay) hiển thị trên trang.

--------------------------------------------------------------------------------
FILE 2: tests/Browser/GamePageTest.php  (12 tests)
--------------------------------------------------------------------------------

[TC-02] test_game_page_loads_when_guest
  Mô tả  : Trang game tải được khi người dùng chưa đăng nhập.
  Bước   : 1. Truy cập "/game" với vai trò khách (guest).
  Kết quả: URL path là "/game" (trang không bị redirect).

[TC-03] test_root_redirects_to_game
  Mô tả  : URL gốc "/" hiển thị cùng nội dung với "/game".
  Bước   : 1. Truy cập "/".
  Kết quả: Phần tử #wheelCanvas (canvas vòng quay) hiển thị.

[TC-04] test_game_page_shows_lucky_wheel_canvas
  Mô tả  : Canvas vòng quay may mắn hiển thị trên trang game.
  Bước   : 1. Truy cập "/game".
  Kết quả: Phần tử #wheelCanvas hiển thị trong trang.

[TC-05] test_spin_button_is_present
  Mô tả  : Nút "Quay" hiển thị trên trang game.
  Bước   : 1. Truy cập "/game".
  Kết quả: Phần tử #spinButton hiển thị trong trang.

[TC-06] test_game_rule_ribbon_is_present
  Mô tả  : Banner "Thể lệ tham dự" hiển thị trên trang game.
  Bước   : 1. Truy cập "/game".
  Kết quả: Phần tử #game_rule hiển thị trong trang.

[TC-07] test_guest_sees_login_prompt_on_game
  Mô tả  : Khách chưa đăng nhập nhìn thấy thông báo yêu cầu đăng nhập.
  Bước   : 1. Đăng xuất ("/logout").
             2. Truy cập "/game".
  Kết quả: Thông báo ".alert_wheel_spin" hiển thị;
            Link đăng nhập (href chứa "login") hiển thị.

[TC-08] test_logged_in_user_can_see_spin_button_enabled
  Mô tả  : Người dùng đã đăng nhập và chưa quay thấy nút quay ở trạng thái sẵn sàng.
  Bước   : 1. Đăng xuất và vào "/login".
             2. Nhập thông tin cá nhân (họ tên, SĐT mới, địa chỉ, nguồn biết SIS, nghề nghiệp).
             3. Nhấn "Gửi thông tin" và chờ redirect về "/game".
  Kết quả: Thuộc tính data-check của #spinButton = "true" (nút quay được bật).
  Dọn dẹp: Xóa user vừa tạo khỏi DB.

[TC-09] test_already_spined_user_sees_spin_button_disabled
  Mô tả  : Người dùng đã quay (spined=1) thấy nút quay bị khóa.
  Chuẩn bị: Tạo user với spined=1 trực tiếp trong DB.
  Bước   : 1. Đăng nhập bằng user có spined=1.
             2. Truy cập "/game".
  Kết quả: Thuộc tính data-check của #spinButton = "false" (nút quay bị khóa).
  Dọn dẹp: Xóa user test khỏi DB.

[TC-10] test_game_rule_popup_loads
  Mô tả  : Popup thể lệ tham dự tải nội dung khi nhấn banner.
  Bước   : 1. Truy cập "/game".
             2. Xóa các element ".alert" bằng JavaScript (để không che banner).
             3. Nhấn vào #game_rule bằng JavaScript click.
             4. Chờ 800ms.
  Kết quả: Phần tử #gamerule (popup) hiển thị trong trang.

[TC-11] test_game_rule_api_returns_html
  Mô tả  : API /game/rule trả về nội dung HTML (dùng cho AJAX).
  Bước   : 1. Truy cập "/game/rule" trực tiếp trên trình duyệt.
  Kết quả: Source HTML của trang chứa chuỗi "class=" (là fragment HTML hợp lệ).

[TC-12] test_checkspined_returns_error_for_guest
  Mô tả  : API /checkspined trả lỗi khi người dùng chưa đăng nhập.
  Bước   : 1. Đăng xuất ("/logout").
             2. Truy cập "/checkspined".
  Kết quả: Trang hiển thị chuỗi "error".

[TC-13] test_checkspined_returns_zero_for_new_user
  Mô tả  : API /checkspined trả spined=0 cho người dùng mới chưa quay.
  Chuẩn bị: Tạo user mới với spined=0 trong DB.
  Bước   : 1. Đăng nhập bằng user đó.
             2. Truy cập "/checkspined".
  Kết quả: Trang hiển thị chuỗi '"spined":0'.
  Dọn dẹp: Xóa user test khỏi DB.

--------------------------------------------------------------------------------
FILE 3: tests/Browser/CreateAccountTest.php  (5 tests)
--------------------------------------------------------------------------------

[TC-14] test_login_page_shows_form
  Mô tả  : Trang đăng ký/đăng nhập hiển thị đủ các trường nhập liệu.
  Bước   : 1. Đăng xuất để đảm bảo trạng thái guest.
             2. Truy cập "/login".
  Kết quả: Tiêu đề "Nhập thông tin cá nhân" hiển thị;
            Các trường input[name="name"], input[name="phone"],
            input[name="address"], input[name="job"] đều có mặt.ss

[TC-15] test_empty_form_shows_validation_errors
  Mô tả  : Gửi form rỗng phải hiện lỗi validation từ server.
  Bước   : 1. Đăng xuất và truy cập "/login".
             2. Nhấn "Gửi thông tin" mà không nhập gì.
             3. Chờ phần tử ".text-danger" xuất hiện.
  Kết quả: Ít nhất một phần tử ".text-danger" (thông báo lỗi) hiển thị.

[TC-16] test_invalid_phone_shows_error
  Mô tả  : Số điện thoại sai định dạng phải hiện thông báo lỗi.
  Bước   : 1. Truy cập "/login".
             2. Nhập tên hợp lệ, nhập SĐT = "12345" (sai định dạng).
             3. Điền các trường còn lại và nhấn "Gửi thông tin".
  Kết quả: Trang hiển thị "Số điện thoại không đúng định dạng".

[TC-17] test_successful_registration_redirects_to_game
  Mô tả  : Đăng ký thành công tạo tài khoản mới và chuyển về trang game.
  Bước   : 1. Truy cập "/login".
             2. Nhập: tên="Test Dusk User", SĐT=số ngẫu nhiên (09xxxxxxxx),
                địa chỉ="Cần Thơ", nguồn biết SIS=1, nghề nghiệp="Kỹ sư".
             3. Nhấn "Gửi thông tin" và chờ thông báo "thành công".
  Kết quả: URL path là "/game" (đăng ký thành công, redirect về game).
  DB Check: User có phone tương ứng tồn tại trong DB;
             name = "TEST DUSK USER" (đã uppercase);
             address = "Cần Thơ"; known_sis = 1; job = "Kỹ sư";
             email = "<phone>@phone.local"; spined = 0; status = 1.
  Dọn dẹp: Xóa user test khỏi DB.

[TC-18] test_authenticated_user_sees_account_page
  Mô tả  : Người dùng đã đăng nhập truy cập /login sẽ thấy trang tài khoản của họ.
  Bước   : 1. Đăng ký tài khoản mới (SĐT ngẫu nhiên).
             2. Sau khi đăng nhập, truy cập lại "/login".
  Kết quả: Trường phone được điền sẵn số điện thoại của người dùng
            (chứng tỏ đang hiển thị form cập nhật tài khoản, không phải form đăng ký).
  DB Check: User tồn tại; spined = 0; status = 1.
  Dọn dẹp: Xóa user test khỏi DB.

--------------------------------------------------------------------------------
FILE 4: tests/Browser/AccountManagementTest.php  (5 tests)
--------------------------------------------------------------------------------

[TC-19] test_update_account_successfully
  Mô tả  : Người dùng cập nhật thông tin tài khoản thành công.
  Bước   : 1. Đăng ký và đăng nhập tài khoản mới.
             2. Truy cập "/login" (trang tài khoản).
             3. Xóa và nhập lại: tên="Tên Mới Test", địa chỉ="Hà Nội",
                nguồn biết SIS=0, nghề nghiệp="Giáo viên".
             4. Nhấn "Cập nhật" và chờ thông báo "thành công".
  Kết quả: URL path là "/game" (cập nhật thành công).
  DB Check: name = "TÊN MỚI TEST" (đã uppercase); address = "Hà Nội";
             known_sis = 0; job = "Giáo viên".
  Dọn dẹp: Xóa user test khỏi DB.

[TC-20] test_update_account_with_empty_name_shows_error
  Mô tả  : Cập nhật tài khoản với tên rỗng phải hiện lỗi validation.
  Bước   : 1. Đăng ký và đăng nhập tài khoản mới.
             2. Truy cập "/login", xóa trường tên.
             3. Nhấn "Cập nhật".
  Kết quả: Phần tử ".text-danger" (thông báo lỗi) hiển thị.

[TC-21] test_update_account_with_invalid_phone_shows_error
  Mô tả  : Cập nhật tài khoản với số điện thoại sai định dạng phải hiện lỗi.
  Bước   : 1. Đăng ký và đăng nhập tài khoản mới.
             2. Truy cập "/login", xóa và nhập SĐT = "abcde".
             3. Nhấn "Cập nhật".
  Kết quả: Trang hiển thị "Số điện thoại không đúng định dạng".

[TC-22] test_history_section_visible_when_authenticated
  Mô tả  : Phần lịch sử hoạt động hiển thị khi người dùng đã đăng nhập.
  Bước   : 1. Đăng ký và đăng nhập tài khoản mới.
             2. Truy cập "/login" (trang tài khoản).
  Kết quả: Trang hiển thị "LỊCH SỬ HOẠT ĐỘNG".

[TC-23] test_logout_works
  Mô tả  : Đăng xuất thành công xóa session người dùng.
  Bước   : 1. Đăng ký và đăng nhập tài khoản mới.
             2. Truy cập "/logout".
  Kết quả: Người dùng ở trạng thái guest (assertGuest).

--------------------------------------------------------------------------------
FILE 5: tests/Browser/AdminLoginTest.php  (7 tests)
--------------------------------------------------------------------------------
Chuẩn bị chung: Trước mỗi test tạo admin user với đầy đủ roles trong DB.
Dọn dẹp chung: Sau mỗi test xóa UserRole và admin user khỏi DB.

[TC-24] test_admin_login_page_shows_form
  Mô tả  : Trang quản trị "/admin" hiển thị form đăng nhập.
  Bước   : 1. Truy cập "/admin".
  Kết quả: Các trường input[name="email"], input[name="password"],
            input[type="submit"] đều hiển thị.

[TC-25] test_admin_login_with_wrong_credentials_shows_error
  Mô tả  : Đăng nhập admin với thông tin sai phải hiện thông báo lỗi.
  Bước   : 1. Truy cập "/admin".
             2. Nhập email="wrong@email.com", password="wrongpassword".
             3. Nhấn "Login".
  Kết quả: Phần tử ".alert-danger" (hoặc tương đương) hiển thị.

[TC-26] test_admin_login_with_empty_fields_shows_validation
  Mô tả  : Gửi form đăng nhập admin rỗng phải hiện lỗi validation server-side.
  Bước   : 1. Truy cập "/admin".
             2. Dùng JavaScript xóa thuộc tính "required" khỏi tất cả input
                (để bypass validation HTML5 của trình duyệt).
             3. Nhấn "Login" mà không nhập gì.
  Kết quả: Phần tử ".text-danger" hoặc ".alert-danger" hiển thị.

[TC-27] test_admin_login_successful
  Mô tả  : Đăng nhập admin thành công và chuyển vào trang quản lý.
  Bước   : 1. Đăng xuất admin ("/admin/logout").
             2. Truy cập "/admin", nhập email và password đúng.
             3. Nhấn "Login" và chờ redirect.
  Kết quả: URL path là "/admin/account" (đăng nhập thành công).

[TC-28] test_authenticated_admin_visiting_login_sees_form
  Mô tả  : Admin đã đăng nhập truy cập "/admin" vẫn thấy form login (không redirect).
  Bước   : 1. Đăng nhập bằng loginAs(adminUser).
             2. Truy cập "/admin".
  Kết quả: URL path vẫn là "/admin"; form input[name="email"] hiển thị.

[TC-29] test_unauthenticated_access_to_admin_account_redirects
  Mô tả  : Truy cập trang admin khi chưa đăng nhập bị redirect về trang login.
  Bước   : 1. Đăng xuất admin.
             2. Truy cập "/admin/account" trực tiếp.
  Kết quả: URL path là "/admin" (bị redirect về trang đăng nhập admin).

[TC-30] test_admin_logout
  Mô tả  : Đăng xuất admin thành công — sau đó không thể vào trang quản lý.
  Bước   : 1. Đăng xuất admin (clear session cũ).
             2. Đăng nhập thực qua form với email/password đúng.
             3. Chờ redirect vào "/admin/account".
             4. Đăng xuất qua "/logout" (frontend route).
             5. Truy cập "/admin/account".
  Kết quả: URL path là "/admin" (bị redirect về login, xác nhận đã logout).
  Lưu ý  : Dùng đăng nhập form thực (không dùng loginAs) để tránh Dusk
            tự động re-authenticate sau khi logout.

--------------------------------------------------------------------------------
FILE 6: tests/Browser/AdminPrizeTest.php  (9 tests)
--------------------------------------------------------------------------------
Chuẩn bị chung: Tạo admin user với đầy đủ roles trong DB.
Dọn dẹp chung: Xóa UserRole và admin user khỏi DB.

[TC-31] test_prize_list_page_loads
  Mô tả  : Trang danh sách giải thưởng tải thành công.
  Bước   : 1. Đăng nhập admin.
             2. Truy cập "/admin/prizes".
  Kết quả: Phần tử "table" hoặc ".table" hiển thị.

[TC-32] test_add_prize_form_loads
  Mô tả  : Trang thêm mới giải thưởng hiển thị đầy đủ form.
  Bước   : 1. Đăng nhập admin.
             2. Truy cập "/admin/prize/update?id=0".
  Kết quả: Các trường input[name="prize_name"], input[name="prize_order"],
            select[name="prize_status"] đều hiển thị.

[TC-33] test_add_prize_with_empty_name_shows_error
  Mô tả  : Gửi form thêm giải thưởng với tên rỗng phải hiện lỗi validation.
  Bước   : 1. Đăng nhập admin, vào "/admin/prize/update?id=0".
             2. Dùng JavaScript xóa thuộc tính "required" của các input.
             3. Nhấn "Thêm mới" mà không nhập tên.
  Kết quả: Phần tử ".text-danger", ".alert-danger" hoặc tương đương hiển thị.

[TC-34] test_create_prize_successfully
  Mô tả  : Thêm mới giải thưởng thành công và dữ liệu được lưu vào DB.
  Bước   : 1. Đăng nhập admin, vào "/admin/prize/update?id=0".
             2. Nhập: prize_name=tên ngẫu nhiên, prize_number=100,
                prize_order=99, prize_status=1.
             3. Nhấn "Thêm mới" và chờ ".alert-success".
  Kết quả: Thông báo thành công hiển thị.
  DB Check: Prize với tên vừa nhập tồn tại trong DB;
             prize_status = 1; prize_number = 100; prize_order = 99.
  Dọn dẹp: Xóa prize test khỏi DB.

[TC-35] test_update_prize_successfully
  Mô tả  : Cập nhật tên giải thưởng thành công.
  Chuẩn bị: Tạo prize "Dusk Prize To Update" trực tiếp trong DB.
  Bước   : 1. Đăng nhập admin, vào "/admin/prize/update?id={prizeId}".
             2. Xóa và nhập tên mới vào prize_name.
             3. Nhấn "Cập nhật" và chờ ".alert-success".
  Kết quả: Thông báo thành công hiển thị.
  DB Check: Prize::find(id)->prize_name bằng tên mới vừa nhập.
  Dọn dẹp: Xóa prize test khỏi DB.

[TC-36] test_delete_prize_successfully
  Mô tả  : Xóa giải thưởng thành công khỏi hệ thống.
  Chuẩn bị: Tạo prize "Dusk Prize To Delete" trực tiếp trong DB.
  Bước   : 1. Đăng nhập admin, vào "/admin/prizes".
             2. Truy cập "/admin/prize/delete/{prizeId}" trực tiếp.
             3. Chờ 1000ms (hệ thống redirect sau khi xóa).
  Kết quả: Prize::find(prizeId) trả về null (đã bị xóa khỏi DB).

[TC-37] test_prize_winner_list_loads
  Mô tả  : Trang danh sách người trúng giải tải thành công.
  Bước   : 1. Đăng nhập admin.
             2. Truy cập "/admin/prizeuser".
  Kết quả: Phần tử "table" hoặc ".table" hiển thị.

[TC-38] test_prize_winner_list_has_date_filter
  Mô tả  : Trang danh sách người trúng giải có bộ lọc theo ngày.
  Bước   : 1. Đăng nhập admin, truy cập "/admin/prizeuser".
  Kết quả: Phần tử #fromDateTime và #toDateTime hiển thị.

[TC-39] test_prize_winner_export_link_present
  Mô tả  : Trang danh sách người trúng giải có nút/link xuất Excel.
  Bước   : 1. Đăng nhập admin, truy cập "/admin/prizeuser".
  Kết quả: Phần tử #excel_export hiển thị.

--------------------------------------------------------------------------------
FILE 7: tests/Browser/AdminUserTest.php  (5 tests)
--------------------------------------------------------------------------------
Chuẩn bị chung: Tạo admin user (với đầy đủ roles) và target user trong DB.
Dọn dẹp chung: Xóa UserRole, admin user và target user khỏi DB.

[TC-40] test_user_list_page_loads
  Mô tả  : Trang danh sách người dùng trong admin tải thành công.
  Bước   : 1. Đăng nhập admin.
             2. Truy cập "/admin/users".
  Kết quả: Phần tử "table" hoặc ".table" hiển thị.

[TC-41] test_user_list_shows_target_user
  Mô tả  : User mới tạo xuất hiện trong danh sách quản lý người dùng.
  Bước   : 1. Truy vấn DB: User với status != '' (điều kiện của controller).
             2. Đăng nhập admin, truy cập "/admin/users", chờ #table.
  Kết quả: Target user tồn tại trong query DB (email khớp);
            Bảng #table hiển thị trên trang.
  Lưu ý  : DataTables có phân trang; dùng DB assertion thay vì tìm kiếm text
            trực tiếp trên trình duyệt.

[TC-42] test_edit_user_form_shows_existing_data
  Mô tả  : Form cập nhật user điền sẵn thông tin hiện tại.
  Bước   : 1. Đăng nhập admin.
             2. Truy cập "/admin/users/update?id={targetUserId}".
  Kết quả: Trường input "name" có giá trị = tên hiện tại của user;
            Trường input "email" có giá trị = email hiện tại của user.

[TC-43] test_update_user_name_successfully
  Mô tả  : Admin cập nhật tên người dùng thành công (không uppercase).
  Bước   : 1. Đăng nhập admin, vào "/admin/users/update?id={targetUserId}".
             2. Xóa và nhập tên mới = "Updated Dusk Name".
             3. Nhấn "Cập nhật" và chờ ".alert-success".
  Kết quả: Thông báo thành công hiển thị.
  DB Check: User::find(id)->name = "Updated Dusk Name"
            (Admin controller KHÔNG uppercase tên, khác với frontend controller).

[TC-44] test_admin_account_page_shows_user_info
  Mô tả  : Trang tài khoản admin hiển thị đúng thông tin người dùng đang đăng nhập.
  Bước   : 1. Đăng nhập admin.
             2. Truy cập "/admin/account".
  Kết quả: Trường input "email" có giá trị = email của admin đang đăng nhập.

--------------------------------------------------------------------------------
FILE 8: tests/Browser/AdminParameterTest.php  (5 tests)
--------------------------------------------------------------------------------
Chuẩn bị chung: Tạo admin user với đầy đủ roles trong DB.
Dọn dẹp chung: Xóa UserRole và admin user khỏi DB.

[TC-45] test_parameter_page_loads
  Mô tả  : Trang cấu hình hệ thống (parameter) tải thành công.
  Bước   : 1. Đăng nhập admin.
             2. Truy cập "/admin/parameter".
  Kết quả: Phần tử "form" hiển thị trong trang.

[TC-46] test_parameter_page_shows_game_rule_field
  Mô tả  : Trang cấu hình có trường nhập "Thể lệ tham dự" (game_rule).
  Bước   : 1. Đăng nhập admin, truy cập "/admin/parameter".
  Kết quả: Phần tử [name="game_rule"] hoặc textarea hiển thị.

[TC-47] test_update_game_rule_successfully
  Mô tả  : Cập nhật nội dung thể lệ tham dự thành công và lưu vào DB.
  Chuẩn bị: Đọc và lưu giá trị game_rule hiện tại từ DB.
  Bước   : 1. Đăng nhập admin, truy cập "/admin/parameter".
             2. Dùng JavaScript gán giá trị mới vào [name="game_rule"]
                (hỗ trợ cả CKEditor nếu có).
             3. Nhấn "Cập nhật" và chờ ".alert-success".
  Kết quả: Thông báo thành công hiển thị.
  DB Check: Parameter với key="game_rule" tồn tại;
             parameter_value chứa chuỗi vừa nhập (assertStringContainsString).
  Dọn dẹp: Khôi phục giá trị game_rule gốc trong DB.

[TC-48] test_parameter_page_has_image_upload_fields
  Mô tả  : Trang cấu hình có trường upload ảnh logo.
  Bước   : 1. Đăng nhập admin, truy cập "/admin/parameter".
  Kết quả: Phần tử input[type="file"][name="logo_image"] hoặc
            input[name="logo_image"] hiển thị.

[TC-49] test_parameter_page_requires_auth
  Mô tả  : Truy cập "/admin/parameter" khi chưa đăng nhập bị redirect.
  Bước   : 1. Đăng xuất admin ("/admin/logout").
             2. Truy cập "/admin/parameter" trực tiếp.
  Kết quả: URL path là "/admin" (bị redirect về trang đăng nhập admin).

--------------------------------------------------------------------------------
FILE 9: tests/Browser/GameLogicTest.php  (3 tests)
--------------------------------------------------------------------------------
Chuẩn bị chung: Tạo user mới (spined=0) và một prize có prize_number=9999 trong DB.
Dọn dẹp chung: Xóa PrizeUser, gameUser và testPrize khỏi DB sau mỗi test.

[TC-50] test_prevent_double_click_spam_on_spin_button
  Mô tả  : Click liên tiếp nhiều lần lên #spinButton chỉ tạo đúng 1 lần quay
            nhờ cờ bảo vệ isSpinning trong game.js.
  Cơ chế : let isSpinning = false; → click đầu tiên set isSpinning = true;
            các click sau bị block bởi guard if (!isSpinning).
  Bước   : 1. Đăng nhập user chưa quay (spined=0), truy cập "/game".
             2. Chờ 800ms để wheel canvas render xong.
             3. Dùng JavaScript gọi spinButton.click() 4 lần liên tiếp.
             4. Chờ popup kết quả "XIN CHÚC MỪNG" xuất hiện (timeout 30s).
             5. Chờ thêm 1500ms để DB transaction commit hoàn tất.
  Kết quả: Browser: popup "XIN CHÚC MỪNG" hiển thị.
  DB Check: PrizeUser::where('user_id', ...) count = 1
            (chứng minh 3 click sau bị ngăn — chỉ 1 lần quay được ghi nhận).
  Lưu ý  : Spin thực mất 10–15s; waitForText timeout = 30s.

[TC-51] test_complete_spin_reward_flow
  Mô tả  : Luồng quay đầy đủ: click → animation → popup kết quả → DB cập nhật.
  Bước   : 1. Đăng nhập user chưa quay, vào "/game".
             2. Chờ 800ms để wheel render.
             3. Xác nhận #spinButton có data-check="true".
             4. Click #spinButton (1 lần).
             5. Chờ text "XIN CHÚC MỪNG" trong div#result (AJAX response từ
                /updateprizeuser, render view luckywheel_prize.blade.php).
             6. Chờ thêm 1500ms để DB write hoàn tất.
  Kết quả: Browser: div#result hiển thị "XIN CHÚC MỪNG".
  DB Check: User::find(id)->spined = 1 (đã được cập nhật sau khi quay);
             PrizeUser::where('user_id', id)->first() != null;
             PrizeUser.user_id khớp với id của người dùng vừa quay.
  Lưu ý  : Selector tương ứng ".reward-popup-class" trong yêu cầu ban đầu
            ánh xạ tới div#result + text "XIN CHÚC MỪNG" trong app này.

[TC-52] test_spin_when_out_of_stock
  Mô tả  : Kiểm tra trạng thái trang game khi TẤT CẢ giải thưởng hết hàng
            (prize_number = 0 cho mọi prize).
  Chuẩn bị: Lưu prize_number gốc của tất cả prizes → update prize_number = 0.
  Bước   : 1. Đăng nhập user, vào "/game".
             2. Chờ 500ms.
  Kết quả: Source HTML chứa 'game_data = "[]"' (IndexController không thêm
            segment nào vì count_user >= prize_number với mọi prize_number = 0).
            Trang vẫn tải bình thường: #wheelCanvas và #spinButton hiển thị.
  Dọn dẹp: Khôi phục tất cả prize_number về giá trị gốc trong finally block.
  TODO   : Khi implement xử lý "hết giải" vào frontend (assertSee "Đã hết giải
            thưởng" / assertPresent ".no-prize-message"), bỏ comment assertions
            tương ứng trong test.

--------------------------------------------------------------------------------
FILE 10: tests/Browser/SecurityTest.php  (2 tests)
--------------------------------------------------------------------------------
Chuẩn bị chung: Tạo regularUser (không có role) và adminUser (đầy đủ roles).
Dọn dẹp chung: Xóa UserRole, adminUser, regularUser khỏi DB sau mỗi test.

[TC-53] test_regular_user_cannot_access_admin_area
  Mô tả  : Người dùng đã đăng nhập nhưng không có role admin bị redirect
            khỏi mọi route trong middleware UserRole.
  Cơ chế : Middleware UserRole → constructMenuLeft() → menu rỗng nếu không
            có UserRole record → mọi admin route bị chặn → redirect('admin').
  Bước   : 1. Đăng nhập bằng regularUser (loginAs).
             2. Truy cập "/admin/account".
             3. Truy cập "/admin/users".
             4. Truy cập "/admin/prizes".
  Kết quả: Cả 3 lần truy cập đều redirect về path "/admin"
            (trang đăng nhập admin, không phải trang quản lý).

[TC-54] test_xss_injection_on_registration_form
  Mô tả  : Nhập XSS payload vào trường name khi đăng ký — ứng dụng phải
            escape đúng cách, không thực thi JavaScript.
  Payload: name = <script>alert('XSS')</script>
  Bước   : 1. Đăng ký tài khoản mới với name = payload XSS.
             2. Đăng xuất.
             3. Đăng nhập admin → vào "/admin/users" → chờ #table.
             4. Dismiss mọi alert dialog (DataTables warnings) nếu có.
  Kết quả: Source HTML KHÔNG chứa "<script>alert('XSS')</script>" (unescaped);
            Source HTML KHÔNG chứa "<SCRIPT>ALERT('XSS')</SCRIPT>" (unescaped);
            Source HTML CÓ CHỨA "&lt;SCRIPT&gt;" (Blade {{ }} đã HTML-escape).
  Cơ chế : Blade {{ }} tự escape → Controller mb_strtoupper → lưu DB → render
            escaped. Payload thực tế trong DB: <SCRIPT>ALERT('XSS')</SCRIPT>.
            Output trong HTML: &lt;SCRIPT&gt;ALERT(&#039;XSS&#039;)&lt;/SCRIPT&gt;.
  Lưu ý  : Dùng helper dismissAnyAlert() trước khi assert để tránh Chrome
            bị lock bởi DataTables errMode alert dialog còn mở.
  Sửa đi kèm: public/backend/js/main.js → DataTable.ext.errMode = 'none'
               (tắt alert dialog của DataTables, dùng silent fail thay thế).
  Dọn dẹp: Xóa user XSS test khỏi DB trong finally block.

--------------------------------------------------------------------------------
FILE 11: tests/Browser/AdminEdgeCaseTest.php  (3 tests)
--------------------------------------------------------------------------------
Chuẩn bị chung: Tạo adminUser với đầy đủ roles trong DB.
Dọn dẹp chung: Xóa UserRole, adminUser khỏi DB; xóa prize test nếu còn sót.
Chuẩn bị chung: Tạo adminUser với đầy đủ roles trong DB.
Dọn dẹp chung: Xóa UserRole, adminUser khỏi DB; xóa prize test nếu còn sót.

[TC-55] test_prize_number_cannot_be_negative
  Mô tả  : Nhập prize_number âm (-5) vào form thêm giải thưởng phải bị
            server từ chối bằng validation error.
  Sửa app: PrizeController::updatePrize() thêm 'prize_number' => 'nullable|numeric|min:0'
            + message 'Số lượng giải thưởng không được âm'.
  Bước   : 1. Đăng nhập admin, vào "/admin/prize/update?id=0".
             2. Dùng JavaScript xóa thuộc tính required và min khỏi input
                (bypass HTML5 client-side validation).
             3. Nhập prize_name hợp lệ, prize_number=-5, prize_order=10.
             4. Nhấn "Thêm mới".
  Kết quả: Phần tử ".text-danger" hoặc ".alert-danger" hiển thị (validation error).
  DB Check: Prize::where('prize_name', testName)->first() = null
            (Prize với số âm KHÔNG được tạo trong DB).

[TC-56] test_invalid_logo_upload_is_rejected
  Mô tả  : Upload file PDF vào trường logo_image phải bị server từ chối.
  Sửa app: ParameterController::update() thêm $request->validate([
            'logo_image' => 'nullable|mimes:jpg,png,jpeg,webp,jfif|max:2048']).
            Fix bảo mật OWASP A05: Unrestricted File Upload.
  Bước   : 1. Tạo file PDF giả tạm thời (sys_get_temp_dir()).
             2. Đăng nhập admin, vào "/admin/parameter".
             3. Attach file PDF vào input[name="logo_image"].
             4. Nhấn "Cập nhật".
  Kết quả: Phần tử ".text-danger" hoặc ".alert-danger" hiển thị.
            Trang KHÔNG hiển thị "Cập nhật thành công".
  Dọn dẹp: Xóa file PDF tạm trong finally block.

[TC-57] test_prize_winner_export_excel_response
  Mô tả  : Nút export Excel (#excel_export) tồn tại và href khi click trỏ
            đúng route admin/prizeuser/export; route tồn tại trong hệ thống.
  Lưu ý  : #excel_export là <a> KHÔNG có static href attribute. Href được tạo
            động trong JS onclick: document.createElement("a"); a.href = exportUrl.
            → Không thể dùng attribute('#excel_export', 'href') để kiểm tra.
  Bước A : 1. Đăng nhập admin, vào "/admin/prizeuser".
             2. Xác nhận #excel_export tồn tại.
             3. Dùng JavaScript override document.createElement để capture
                href được gán cho thẻ <a> khi #excel_export.click().
  Kết quả A: capturedHref != null;
              capturedHref chứa chuỗi 'prizeuser/export'.
  Bước B : Gọi route('admin/prizeuser/export') bằng PHP helper.
  Kết quả B: URL trả về chứa '/admin/prizeuser/export'
             (xác nhận route đã được đăng ký trong hệ thống).
  Lưu ý  : Không dùng actingAs()->get(exportRoute) vì excel.blade.php gọi exit()
            sau khi xuất file — điều này sẽ terminate PHPUnit process.

--------------------------------------------------------------------------------
FILE 12: tests/Browser/BulkWorkflowTest.php  (1 test)
--------------------------------------------------------------------------------
Chuẩn bị chung: Tạo adminUser (đầy đủ roles), tạo prize có prize_number=2000,
                tạo file Excel tạm chứa 1000 bản ghi người dùng bằng PhpSpreadsheet.
Dọn dẹp chung: Xóa 1000 users + PrizeUser (theo chunk 200), xóa admin, prize, file Excel.

[TC-58] test_1000_users_full_workflow_from_excel
  Mô tả  : Kiểm tra toàn bộ workflow hệ thống (end-to-end) với 1000 người dùng
            được cung cấp từ file Excel — từ import dữ liệu đến quay số đến
            xác nhận trên giao diện admin.
  Luồng  :
    [A] Đọc file Excel → Xác nhận file có đúng 1001 dòng (1 header + 1000 data).
    [B] Bulk-import 1000 users vào DB (đọc từng ô Excel, insert theo chunk 100).
    [C] Simulate 1000 lượt quay số (bulk insert prize_users + update spined=1).
    [D] 1 lượt quay thực qua HTTP POST /updateprizeuser để kiểm tra API layer.
    [E] Browser: Admin mở /admin/users → bảng #table hiển thị.
        Browser: Admin mở /admin/prizeuser → bảng + bộ lọc ngày + nút export hiển thị.
    [F] Browser: Xác nhận href của #excel_export chứa "prizeuser/export";
        PHP route('admin/prizeuser/export') trả URL hợp lệ.

  Chi tiết từng bước:

  [A] Tạo & Đọc Excel:
      Chuẩn bị: generateTestExcel() dùng PhpOffice\PhpSpreadsheet tạo file .xlsx
                với 6 cột: STT | HỌ TÊN | SỐ ĐIỆN THOẠI | ĐỊA CHỈ | NGHỀ NGHIỆP | NGUỒN BIẾT SIS.
                Phone dùng dải riêng 0900000001..0900001000 (tránh đụng dữ liệu thật).
      Bước    : XlsxReader::load() → getHighestDataRow() = 1001.
      Kết quả : File Excel hợp lệ, đọc được 1001 dòng.

  [B] Bulk-import 1000 users:
      Bước    : Đọc từng row (2..1001) → lấy phone, name (mb_strtoupper), address,
                job, known_sis → build $importBatch[].
                DB::table('users')->insert() theo chunk 100.
                Lấy IDs: User::whereIn('phone', ...)->pluck('id').
      Kết quả : assertCount(1000, $bulkUserIds) — 1000 users được import.

  [C] Simulate 1000 lần quay:
      Bước    : Build $spinBatch[] với user_id + prize_id cho 1000 user.
                DB::table('prize_users')->insert() theo chunk 100.
                User::whereIn('id', chunk)->update(['spined' => 1]) theo chunk 200.
      Kết quả : PrizeUser::whereIn('user_id', $bulkUserIds)->count() = 1000.
                User::whereIn(...)->where('spined', 1)->count() = 1000.

  [D] Kiểm tra luồng spin thực qua Browser (1 user mới, giao diện game):
      Chuẩn bị: Tạo 1 user riêng (phone='0900000000') ngoài dải bulk, spined=0.
      Bước    : browser->loginAs(spinUser) → visit('/game') → pause(800ms)
                → assertAttribute('#spinButton','data-check','true')
                → click('#spinButton') → waitForText('XIN CHÚC MỪNG', 30s)
                → pause(1500ms) để DB commit.
      Kết quả : spinUser->refresh()->spined = 1.
                PrizeUser::where('user_id', spinUserId)->first() != null.
      Lợi thế : Dùng Browser thực → CSRF token có trong session → không bị lỗi 419.
                Đây là cách kiểm tra API /updateprizeuser trung thực nhất.
      Dọn dẹp : Xóa spinUser và PrizeUser của spinUser ngay sau khi assert.

  [E] Browser admin xác nhận:
      Bước    : browser->loginAs($adminUser).
                Truy cập /admin/users → waitFor('#table', 15) → assertPresent('#table').
                Truy cập /admin/prizeuser → waitFor('table, .table', 15)
                → assertPresent('#fromDateTime', '#toDateTime', '#excel_export').
      Kết quả : Bảng quản lý người dùng và bảng người trúng giải đều hiển thị đúng.

  [F] Xác nhận export Excel:
      Bước    : JS override document.createElement để capture href khi click #excel_export.
                PHP: route('admin/prizeuser/export') kiểm tra URL.
      Kết quả : capturedHref chứa 'prizeuser/export' (nếu JS capture thành công).
                route('admin/prizeuser/export') chứa '/admin/prizeuser/export'.
      Lưu ý   : Không gọi actingAs()->get(exportUrl) vì excel.blade.php gọi exit()
                sau khi xuất file → terminate PHPUnit process.

  Dữ liệu test:
    - 1000 users với phone 0900000001..0900001000
    - 1 prize "Giải Thưởng Bulk Test" (prize_number=2000, prize_order=98)
    - 1 admin "DUSK BULK ADMIN" (email: dusk_bulk_admin@test.local)
    - 1 file Excel tạm (sys_get_temp_dir()/dusk_bulk_{timestamp}.xlsx)

  Assertions: 15 assertions
  Thời gian : ~37 giây

================================================================================
TỔNG KẾT
================================================================================

  Số file test  : 12 files
  Số test cases : 58 test cases
  Số assertions : 129 assertions
  Thời gian     : ~215 giây
  Kết quả       : ALL PASSED ✓

  Phân bổ theo chức năng:
  ┌──────────────────────────────────────────┬────────┐
  │ Chức năng                                │  Tests │
  ├──────────────────────────────────────────┼────────┤
  │ ExampleTest (sanity check)               │      1 │
  │ Trang game & API vòng quay               │     12 │
  │ Đăng ký tài khoản                        │      5 │
  │ Quản lý tài khoản người dùng             │      5 │
  │ Đăng nhập / đăng xuất admin             │      7 │
  │ Quản lý giải thưởng (admin)              │      9 │
  │ Quản lý người dùng (admin)               │      5 │
  │ Cấu hình hệ thống (admin)               │      5 │
  │ Logic game nâng cao                      │      3 │
  │ Bảo mật & phân quyền                     │      2 │
  │ Admin edge cases                         │      3 │
  │ Bulk workflow 1000 users từ Excel        │      1 │
  ├──────────────────────────────────────────┼────────┤
  │ TỔNG CỘNG                                │     58 │
  └──────────────────────────────────────────┴────────┘

  Công nghệ kiểm thử:
  - Framework  : Laravel Dusk v8.5 (Browser Automation Testing)
  - Browser    : Google Chrome 146 (non-snap, /usr/bin/google-chrome)
  - Driver     : ChromeDriver 146
  - Môi trường : WSL2 + WSLg, DUSK_HEADLESS_DISABLED=true
  - Server     : php artisan serve --port=8001
  - DB         : MySQL (fsg3kct926qj_quaysoDB)
  - Excel lib  : PhpOffice\PhpSpreadsheet (có sẵn trong vendor/phpoffice/)

  Sửa lỗi ứng dụng phát hiện qua quá trình kiểm thử:
  ┌──────────────────────────────────────────────────────────────────────┐
  │ 1. PrizeController — thiếu min:0 trong prize_number validation       │
  │    → Cho phép tạo giải thưởng với số lượng âm (bug logic)           │
  │ 2. ParameterController — không validate MIME type khi upload ảnh    │
  │    → OWASP A05: Unrestricted File Upload (lỗ hổng bảo mật)         │
  │ 3. DataTables errMode = "alert" trong main.js                       │
  │    → Alert dialog blocking Chrome khi render HTML content trong cell│
  └──────────────────────────────────────────────────────────────────────┘

================================================================================
