/**
 * LearnWorlds Integration Tests — hits the REAL LearnWorlds API.
 *
 * Covers ALL 19 LearnWorlds actions chained together:
 *   Read-only (no setup needed): GetCourses, GetCourseDetails, GetCourseAnalytics, GetBundles, GetUsers
 *   Create/mutate:               CreateUser, UpdateUser, AttachTags, DetachTags
 *   Enrollment:                  EnrollUser, GetUserEnrollments, GetUserProgress, UpdateUserProgress
 *   Assessment & Achievement:    GetCertificates, GetQuizResults
 *   Auth:                        SSOLogin
 *   Details:                     GetUserDetails
 *   Orchestration:               OnboardLearner
 *   Lookup:                      FindUserByEmail
 *
 * PREREQUISITES:
 *   1. Copy .env.integration.example -> .env.integration and fill in real values
 *   2. Run:  npm run test:integration
 *
 * These tests are SKIPPED by default (the `describe.skipIf` guard).
 * They only run when LW_INTEGRATION=true is set in the environment.
 */
import { describe, it, expect, vi } from 'vitest';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';

// ---------- load .env.integration if present ----------
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, '../../.env.integration') });

const RUN_INTEGRATION = process.env.LW_INTEGRATION === 'true';

// ---------- mock only the MJ framework plumbing (not fetch) ----------
vi.mock('@memberjunction/actions', () => ({
  BaseAction: class BaseAction {
    protected async InternalRunAction(): Promise<unknown> {
      return {};
    }
  },
}));

vi.mock('@memberjunction/global', () => ({
  RegisterClass: () => (target: unknown) => target,
  UUIDsEqual: (a: string, b: string) => a?.toLowerCase() === b?.toLowerCase(),
}));

vi.mock('@memberjunction/core', () => ({
  UserInfo: class UserInfo {},
  Metadata: vi.fn(),
  RunView: vi.fn().mockImplementation(() => ({
    RunView: vi.fn().mockResolvedValue({ Success: true, Results: [] }),
  })),
}));

vi.mock('@memberjunction/core-entities', () => ({
  MJCompanyIntegrationEntity: class MJCompanyIntegrationEntity {
    CompanyID = '';
    APIKey: string | null = null;
    AccessToken: string | null = null;
    ExternalSystemID: string | null = null;
    CustomAttribute1: string | null = null;
  },
}));

vi.mock('@memberjunction/actions-base', () => ({
  ActionParam: class ActionParam {
    Name = '';
    Value: unknown = null;
    Type = 'Input';
  },
}));

// ---------- imports (after mocks) ----------
import { CreateUserAction } from '../providers/learnworlds/actions/create-user.action';
import { EnrollUserAction } from '../providers/learnworlds/actions/enroll-user.action';
import { SSOLoginAction } from '../providers/learnworlds/actions/sso-login.action';
import { GetLearnWorldsUsersAction } from '../providers/learnworlds/actions/get-users.action';
import { GetLearnWorldsUserDetailsAction } from '../providers/learnworlds/actions/get-user-details.action';
import { GetUserEnrollmentsAction } from '../providers/learnworlds/actions/get-user-enrollments.action';
import { GetLearnWorldsUserProgressAction } from '../providers/learnworlds/actions/get-user-progress.action';
import { GetLearnWorldsCoursesAction } from '../providers/learnworlds/actions/get-courses.action';
import { GetLearnWorldsCourseDetailsAction } from '../providers/learnworlds/actions/get-course-details.action';
import { GetBundlesAction } from '../providers/learnworlds/actions/get-bundles.action';
import { UpdateUserAction } from '../providers/learnworlds/actions/update-user.action';
import { AttachTagsAction } from '../providers/learnworlds/actions/attach-tags.action';
import { DetachTagsAction } from '../providers/learnworlds/actions/detach-tags.action';
import { OnboardLearnerAction } from '../providers/learnworlds/actions/onboard-learner.action';
import { GetLearnWorldsBulkDataAction } from '../providers/learnworlds/actions/get-bulk-data.action';
import { GetCourseAnalyticsAction } from '../providers/learnworlds/actions/get-course-analytics.action';
import { GetCertificatesAction } from '../providers/learnworlds/actions/get-certificates.action';
import { GetQuizResultsAction } from '../providers/learnworlds/actions/get-quiz-results.action';
import { UpdateUserProgressAction } from '../providers/learnworlds/actions/update-user-progress.action';
import { LearnWorldsBaseAction } from '../providers/learnworlds/learnworlds-base.action';
import { UserInfo } from '@memberjunction/core';

// ---------- env-driven config ----------
const SCHOOL_DOMAIN = process.env.LW_SCHOOL_DOMAIN || '';
const API_KEY = process.env.LW_API_KEY || '';
const CLIENT_ID = process.env.LW_CLIENT_ID || '';
const COMPANY_ID = process.env.LW_COMPANY_ID || '00000000-0000-0000-0000-000000000001';
const TEST_COURSE_ID = process.env.LW_TEST_COURSE_ID || '';
const TEST_LESSON_ID = process.env.LW_TEST_LESSON_ID || '';
const TEST_EMAIL = process.env.LW_TEST_EMAIL || `mj-test-${Date.now()}@example.com`;
const ONBOARD_EMAIL = `mj-onboard-${Date.now()}@example.com`;

/**
 * Helper: patch `getCompanyIntegration` and `getAPICredentials` so we
 * bypass the database and use env-var credentials directly.
 */
function patchCredentials<T extends LearnWorldsBaseAction>(action: T): T {
  const a = action as Record<string, unknown>;

  a['getCompanyIntegration'] = async () => ({
    CompanyID: COMPANY_ID,
    APIKey: API_KEY,
    ExternalSystemID: SCHOOL_DOMAIN,
    AccessToken: null,
    CustomAttribute1: null,
  });

  a['getAPICredentials'] = async () => ({
    apiKey: API_KEY,
    apiSecret: undefined,
    accessToken: undefined,
  });

  // Seed the CLIENT_ID env var so buildRequestConfig resolves it
  const provider = 'LEARNWORLDS';
  process.env[`BIZAPPS_${provider}_${COMPANY_ID}_CLIENT_ID`] = CLIENT_ID || SCHOOL_DOMAIN;

  return action;
}

function mockContextUser(): UserInfo {
  return { ID: 'integration-test-user' } as unknown as UserInfo;
}

// ──────────────────────────────────────────────────────────────────────
// Integration suite — only runs when LW_INTEGRATION=true
// ──────────────────────────────────────────────────────────────────────
describe.skipIf(!RUN_INTEGRATION)('LearnWorlds Integration Tests', () => {
  const contextUser = mockContextUser();
  let createdUserId: string | undefined;
  let onboardedUserId: string | undefined;

  // ═══════════════════════════════════════════════════════════════════
  // PHASE 1: Read-only actions (no setup needed)
  // ═══════════════════════════════════════════════════════════════════

  describe('Phase 1: Read-only actions', () => {
    // ─── GetCourses ──────────────────────────────────────────────────
    it('GetCourses — should list courses', async () => {
      const action = patchCredentials(new GetLearnWorldsCoursesAction());
      const result = await action.GetCourses({ CompanyID: COMPANY_ID, MaxResults: 5 }, contextUser);

      expect(result.Courses).toBeDefined();
      expect(Array.isArray(result.Courses)).toBe(true);
      console.log(`  GetCourses: OK (${result.TotalCount} courses returned)`);
    });

    // ─── GetCourseDetails ────────────────────────────────────────────
    it('GetCourseDetails — should get course details', async () => {
      if (!TEST_COURSE_ID) {
        console.warn('  Skipping GetCourseDetails — LW_TEST_COURSE_ID not set');
        return;
      }

      const action = patchCredentials(new GetLearnWorldsCourseDetailsAction());
      const result = await action.GetCourseDetails(
        { CompanyID: COMPANY_ID, CourseID: TEST_COURSE_ID, IncludeModules: false, IncludeInstructors: false, IncludeStats: false },
        contextUser,
      );

      expect(result.CourseDetails).toBeDefined();
      expect(result.CourseDetails.id).toBeTruthy();
      console.log(`  GetCourseDetails: OK (title: "${result.CourseDetails.title}")`);
    });

    // ─── GetCourseAnalytics ────────────────────────────────────────────
    it('GetCourseAnalytics — should get course analytics', async () => {
      if (!TEST_COURSE_ID) {
        console.warn('  Skipping GetCourseAnalytics — LW_TEST_COURSE_ID not set');
        return;
      }

      const action = patchCredentials(new GetCourseAnalyticsAction());
      const result = await action.GetCourseAnalytics(
        {
          CompanyID: COMPANY_ID,
          CourseID: TEST_COURSE_ID,
          IncludeUserBreakdown: false,
          IncludeModuleStats: false,
          IncludeRevenue: false,
        },
        contextUser,
      );

      expect(result.CourseAnalytics).toBeDefined();
      expect(result.Summary).toBeDefined();
      console.log(`  GetCourseAnalytics: OK (enrollments=${result.CourseAnalytics.totalEnrollments}, completionRate=${result.CourseAnalytics.completionRate}%)`);
    });

    // ─── GetBundles ──────────────────────────────────────────────────
    it('GetBundles — should list bundles', async () => {
      const action = patchCredentials(new GetBundlesAction());
      const result = await action.GetBundles({ CompanyID: COMPANY_ID, MaxResults: 5 }, contextUser);

      expect(result.Bundles).toBeDefined();
      expect(Array.isArray(result.Bundles)).toBe(true);
      console.log(`  GetBundles: OK (${result.TotalCount} bundles returned)`);
    });

    // ─── GetUsers ────────────────────────────────────────────────────
    it('GetUsers — should list users', async () => {
      const action = patchCredentials(new GetLearnWorldsUsersAction());
      const result = await action.GetUsers({ CompanyID: COMPANY_ID, MaxResults: 5 }, contextUser);

      expect(result.Users).toBeDefined();
      expect(Array.isArray(result.Users)).toBe(true);
      expect(result.TotalCount).toBeGreaterThan(0);
      console.log(`  GetUsers: OK (${result.TotalCount} users returned)`);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // PHASE 2: User lifecycle (create -> update -> tags -> enroll)
  // ═══════════════════════════════════════════════════════════════════

  describe('Phase 2: User lifecycle', () => {
    // ─── FindUserByEmail (non-existent) ──────────────────────────────
    it('FindUserByEmail — should return null for non-existent user', async () => {
      const action = patchCredentials(new CreateUserAction());
      action.SetCompanyContext(COMPANY_ID);

      const result = await action.FindUserByEmail('nonexistent-integration-test@example.com', contextUser);
      expect(result).toBeNull();
      console.log('  FindUserByEmail: OK (no match, API responded)');
    });

    // ─── CreateUser ──────────────────────────────────────────────────
    it('CreateUser — should create a test user', async () => {
      const action = patchCredentials(new CreateUserAction());

      const result = await action.CreateUser(
        {
          CompanyID: COMPANY_ID,
          Email: TEST_EMAIL,
          FirstName: 'MJ',
          LastName: 'IntegrationTest',
          Role: 'student',
          IsActive: true,
          SendWelcomeEmail: false,
        },
        contextUser,
      );

      expect(result.UserDetails.id).toBeTruthy();
      expect(result.UserDetails.email).toBe(TEST_EMAIL);
      createdUserId = result.UserDetails.id;

      console.log(`  CreateUser: OK (userId=${createdUserId}, username=${result.UserDetails.username})`);
    });

    // ─── UpdateUser ──────────────────────────────────────────────────
    it('UpdateUser — should update the created user', async () => {
      if (!createdUserId) {
        console.warn('  Skipping UpdateUser — no userId');
        return;
      }

      const action = patchCredentials(new UpdateUserAction());

      const result = await action.UpdateUser(
        {
          CompanyID: COMPANY_ID,
          UserID: createdUserId,
          FirstName: 'MJ-Updated',
          LastName: 'IntegrationTest-Updated',
        },
        contextUser,
      );

      expect(result.UserDetails).toBeDefined();
      expect(result.Summary.fieldsUpdated.length).toBeGreaterThan(0);
      console.log(`  UpdateUser: OK (fields updated: ${result.Summary.fieldsUpdated.join(', ')})`);
    });

    // ─── AttachTags ──────────────────────────────────────────────────
    it('AttachTags — should attach tags to the user', async () => {
      if (!createdUserId) {
        console.warn('  Skipping AttachTags — no userId');
        return;
      }

      const action = patchCredentials(new AttachTagsAction());

      const result = await action.AttachTags(
        {
          CompanyID: COMPANY_ID,
          UserID: createdUserId,
          Tags: ['mj-integration-test', 'automated'],
        },
        contextUser,
      );

      expect(result.Success).toBe(true);
      console.log(`  AttachTags: OK (tags: ${result.Tags.join(', ')})`);
    });

    // ─── DetachTags ──────────────────────────────────────────────────
    it('DetachTags — should detach a tag from the user', async () => {
      if (!createdUserId) {
        console.warn('  Skipping DetachTags — no userId');
        return;
      }

      const action = patchCredentials(new DetachTagsAction());

      const result = await action.DetachTags(
        {
          CompanyID: COMPANY_ID,
          UserID: createdUserId,
          Tags: ['automated'],
        },
        contextUser,
      );

      expect(result.Success).toBe(true);
      console.log(`  DetachTags: OK (remaining tags: ${result.Tags.join(', ')})`);
    });

    // ─── EnrollUser ──────────────────────────────────────────────────
    it('EnrollUser — should enroll the created user in a course', async () => {
      if (!createdUserId) {
        console.warn('  Skipping EnrollUser — no userId');
        return;
      }
      if (!TEST_COURSE_ID) {
        console.warn('  Skipping EnrollUser — LW_TEST_COURSE_ID not set');
        return;
      }

      const action = patchCredentials(new EnrollUserAction());

      const result = await action.EnrollUser(
        {
          CompanyID: COMPANY_ID,
          UserID: createdUserId,
          CourseID: TEST_COURSE_ID,
          ProductType: 'course',
          Price: 0,
          Justification: 'MJ integration test',
          NotifyUser: false,
        },
        contextUser,
      );

      expect(result.EnrollmentDetails).toBeTruthy();
      console.log(`  EnrollUser: OK (enrollmentId=${result.EnrollmentDetails.id}, status=${result.EnrollmentDetails.status})`);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // PHASE 3: User data retrieval (needs created+enrolled user)
  // ═══════════════════════════════════════════════════════════════════

  describe('Phase 3: User data retrieval', () => {
    // ─── GetUserDetails ──────────────────────────────────────────────
    it('GetUserDetails — should get full user details', async () => {
      if (!createdUserId) {
        console.warn('  Skipping GetUserDetails — no userId');
        return;
      }

      const action = patchCredentials(new GetLearnWorldsUserDetailsAction());

      const result = await action.GetUserDetails(
        { CompanyID: COMPANY_ID, UserID: createdUserId, IncludeEnrollments: true, IncludeStats: false },
        contextUser,
      );

      expect(result.UserDetails).toBeDefined();
      expect(result.UserDetails.id).toBe(createdUserId);
      expect(result.UserDetails.email).toBe(TEST_EMAIL);
      console.log(`  GetUserDetails: OK (email=${result.UserDetails.email}, enrollments=${result.UserDetails.enrollments?.length ?? 0})`);
    });

    // ─── GetUserEnrollments ──────────────────────────────────────────
    it('GetUserEnrollments — should list enrollments', async () => {
      if (!createdUserId) {
        console.warn('  Skipping GetUserEnrollments — no userId');
        return;
      }

      const action = patchCredentials(new GetUserEnrollmentsAction());

      const result = await action.GetUserEnrollments(
        { CompanyID: COMPANY_ID, UserID: createdUserId, IncludeCourseDetails: false },
        contextUser,
      );

      expect(result.Enrollments).toBeDefined();
      expect(Array.isArray(result.Enrollments)).toBe(true);
      console.log(`  GetUserEnrollments: OK (${result.TotalCount} enrollments)`);
    });

    // ─── GetUserProgress ─────────────────────────────────────────────
    it('GetUserProgress — should get user progress', async () => {
      if (!createdUserId) {
        console.warn('  Skipping GetUserProgress — no userId');
        return;
      }

      const action = patchCredentials(new GetLearnWorldsUserProgressAction());

      const result = await action.GetUserProgress(
        {
          CompanyID: COMPANY_ID,
          UserID: createdUserId,
          ...(TEST_COURSE_ID ? { CourseID: TEST_COURSE_ID } : {}),
        },
        contextUser,
      );

      expect(result.UserProgress).toBeDefined();
      console.log(`  GetUserProgress: OK (courses=${result.UserProgress.totalCourses}, overall=${result.UserProgress.overallProgressPercentage}%)`);
    });

    // ─── UpdateUserProgress ────────────────────────────────────────────
    it('UpdateUserProgress — should update course progress', async () => {
      if (!createdUserId || !TEST_COURSE_ID) {
        console.warn('  Skipping UpdateUserProgress — no userId or no TEST_COURSE_ID');
        return;
      }

      const action = patchCredentials(new UpdateUserProgressAction());

      try {
        const result = await action.UpdateProgress(
          {
            CompanyID: COMPANY_ID,
            UserID: createdUserId,
            CourseID: TEST_COURSE_ID,
            ...(TEST_LESSON_ID ? { LessonID: TEST_LESSON_ID, Completed: true } : { ProgressPercentage: 10 }),
          },
          contextUser,
        );

        expect(result.ProgressDetails).toBeDefined();
        expect(result.Summary).toBeDefined();
        const updateType = result.ProgressDetails.updateType;
        const newPct = result.Summary.newPercentage;
        console.log(`  UpdateUserProgress: OK (type=${updateType}, newProgress=${newPct}%, completed=${result.Summary.isCompleted})`);
      } catch (error) {
        // LearnWorlds enrollment/progress endpoints may not be available for all course types
        // or may take time to register a new enrollment. Log and pass if it's a 404.
        const msg = error instanceof Error ? error.message : String(error);
        if (msg.includes('not enrolled') || msg.includes('does not exist') || msg.includes('404')) {
          console.warn(`  UpdateUserProgress: SKIPPED (enrollment not ready or endpoint unavailable: ${msg})`);
        } else {
          throw error;
        }
      }
    });

    // ─── GetCertificates ────────────────────────────────────────────
    it('GetCertificates — should list certificates for the user', async () => {
      if (!createdUserId) {
        console.warn('  Skipping GetCertificates — no userId');
        return;
      }

      const action = patchCredentials(new GetCertificatesAction());

      try {
        const result = await action.GetCertificates(
          {
            CompanyID: COMPANY_ID,
            UserID: createdUserId,
            IncludeDownloadLinks: true,
            MaxResults: 10,
          },
          contextUser,
        );

        expect(result.Certificates).toBeDefined();
        expect(Array.isArray(result.Certificates)).toBe(true);
        console.log(`  GetCertificates: OK (${result.TotalCount} certificate(s) found)`);
      } catch (error) {
        // A newly created test user typically has no certificates.
        // The certificates endpoint may return 404 if no certificates exist.
        const msg = error instanceof Error ? error.message : String(error);
        if (msg.includes('does not exist') || msg.includes('404')) {
          console.warn(`  GetCertificates: SKIPPED (no certificates for test user — endpoint returned 404)`);
        } else {
          throw error;
        }
      }
    });

    // ─── GetQuizResults ─────────────────────────────────────────────
    it('GetQuizResults — should list quiz results for the user', async () => {
      if (!createdUserId) {
        console.warn('  Skipping GetQuizResults — no userId');
        return;
      }

      const action = patchCredentials(new GetQuizResultsAction());

      try {
        const result = await action.GetQuizResults(
          {
            CompanyID: COMPANY_ID,
            UserID: createdUserId,
            IncludeQuestions: false,
            IncludeAnswers: false,
            MaxResults: 10,
          },
          contextUser,
        );

        expect(result.QuizResults).toBeDefined();
        expect(Array.isArray(result.QuizResults)).toBe(true);
        console.log(`  GetQuizResults: OK (${result.TotalCount} quiz result(s) found)`);
      } catch (error) {
        // A newly created test user has no quiz attempts.
        // The quiz-results endpoint may return 404.
        const msg = error instanceof Error ? error.message : String(error);
        if (msg.includes('does not exist') || msg.includes('404')) {
          console.warn(`  GetQuizResults: SKIPPED (no quiz results for test user — endpoint returned 404)`);
        } else {
          throw error;
        }
      }
    });

    // ─── FindUserByEmail (verify) ────────────────────────────────────
    it('FindUserByEmail (verify) — should find the user we created', async () => {
      if (!createdUserId) {
        console.warn('  Skipping FindUserByEmail verify — no userId');
        return;
      }

      const action = patchCredentials(new CreateUserAction());
      action.SetCompanyContext(COMPANY_ID);

      const result = await action.FindUserByEmail(TEST_EMAIL, contextUser);

      expect(result).not.toBeNull();
      expect(result!.email.toLowerCase()).toBe(TEST_EMAIL.toLowerCase());
      console.log(`  FindUserByEmail (verify): OK (found user id=${result!.id})`);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // PHASE 4: SSO Login
  // ═══════════════════════════════════════════════════════════════════

  describe('Phase 4: SSO Login', () => {
    it('SSOLogin — should generate an SSO URL', async () => {
      const action = patchCredentials(new SSOLoginAction());

      const result = await action.GenerateSSOUrl(
        { CompanyID: COMPANY_ID, Email: TEST_EMAIL },
        contextUser,
      );

      expect(result.LoginURL).toBeTruthy();
      expect(result.LoginURL).toContain('http');
      console.log(`  SSOLogin: OK (loginURL=${result.LoginURL})`);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // PHASE 5: Orchestration — OnboardLearner
  // ═══════════════════════════════════════════════════════════════════

  describe('Phase 5: OnboardLearner orchestration', () => {
    it('OnboardLearner — should create user + enroll + SSO in one call', async () => {
      const action = patchCredentials(new OnboardLearnerAction());

      // Patch credentials at the prototype level so inner action instances
      // (CreateUserAction, EnrollUserAction, SSOLoginAction) also get them
      const credentialMock = async () => ({
        CompanyID: COMPANY_ID,
        APIKey: API_KEY,
        ExternalSystemID: SCHOOL_DOMAIN,
        AccessToken: null,
        CustomAttribute1: null,
      });
      const apiCredMock = async () => ({
        apiKey: API_KEY,
        apiSecret: undefined,
        accessToken: undefined,
      });
      vi.spyOn(LearnWorldsBaseAction.prototype as Record<string, unknown>, 'getCompanyIntegration' as never).mockImplementation(credentialMock as never);
      vi.spyOn(LearnWorldsBaseAction.prototype as Record<string, unknown>, 'getAPICredentials' as never).mockImplementation(apiCredMock as never);

      const result = await action.OnboardLearner(
        {
          CompanyID: COMPANY_ID,
          Email: ONBOARD_EMAIL,
          FirstName: 'MJ',
          LastName: 'OnboardTest',
          Role: 'student',
          ...(TEST_COURSE_ID ? { CourseIDs: [TEST_COURSE_ID] } : {}),
          SendWelcomeEmail: false,
        },
        contextUser,
      );

      expect(result.Success).toBe(true);
      expect(result.LearnWorldsUserId).toBeTruthy();
      expect(result.IsNewUser).toBe(true);
      onboardedUserId = result.LearnWorldsUserId;

      console.log(`  OnboardLearner: OK`);
      console.log(`    userId    = ${result.LearnWorldsUserId}`);
      console.log(`    isNewUser = ${result.IsNewUser}`);
      console.log(`    loginURL  = ${result.LoginURL}`);
      console.log(`    enrollments = ${result.Enrollments.length} (${result.Enrollments.filter((e) => e.success).length} succeeded)`);
      if (result.Errors.length > 0) {
        console.log(`    errors    = ${result.Errors.join('; ')}`);
      }
    });

    it('OnboardLearner — should find existing user on second call', async () => {
      if (!onboardedUserId) {
        console.warn('  Skipping OnboardLearner re-run — no onboardedUserId');
        return;
      }

      const action = patchCredentials(new OnboardLearnerAction());

      const result = await action.OnboardLearner(
        {
          CompanyID: COMPANY_ID,
          Email: ONBOARD_EMAIL,
          FirstName: 'MJ',
          LastName: 'OnboardTest',
        },
        contextUser,
      );

      expect(result.IsNewUser).toBe(false);
      expect(result.LearnWorldsUserId).toBe(onboardedUserId);
      console.log(`  OnboardLearner (re-run): OK (isNewUser=false, same userId=${result.LearnWorldsUserId})`);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // PHASE 6: Bulk data retrieval — rate limit stress test
  //
  // This is the test that reproduces GitHub issue #2312.
  // It fetches ALL users then hits the enrollment endpoint for each one,
  // which is the exact pattern that triggers 429 rate limiting.
  // ═══════════════════════════════════════════════════════════════════

  describe('Phase 6: GetBulkData rate limit stress test', () => {
    it('GetBulkData — should fetch all users + enrollments without 429 failures', async () => {
      // Reset global rate limiter to ensure a clean sliding window
      LearnWorldsBaseAction.ResetRateLimiter();

      // Seed the CLIENT_ID env var so buildRequestConfig resolves it
      // (normally seeded by patchCredentials() in earlier phases)
      const provider = 'LEARNWORLDS';
      process.env[`BIZAPPS_${provider}_${COMPANY_ID}_CLIENT_ID`] = CLIENT_ID || SCHOOL_DOMAIN;

      // Patch credentials at the prototype level so all inner action instances
      // (GetUsersAction, GetUserEnrollmentsAction, etc.) also get them
      const credentialMock = async () => ({
        CompanyID: COMPANY_ID,
        APIKey: API_KEY,
        ExternalSystemID: SCHOOL_DOMAIN,
        AccessToken: null,
        CustomAttribute1: null,
      });
      const apiCredMock = async () => ({
        apiKey: API_KEY,
        apiSecret: undefined,
        accessToken: undefined,
      });
      vi.spyOn(LearnWorldsBaseAction.prototype as Record<string, unknown>, 'getCompanyIntegration' as never).mockImplementation(credentialMock as never);
      vi.spyOn(LearnWorldsBaseAction.prototype as Record<string, unknown>, 'getAPICredentials' as never).mockImplementation(apiCredMock as never);

      // Track 429 retries via console.warn spy
      const retryWarnings: string[] = [];
      const originalWarn = console.warn;
      console.warn = (...args: unknown[]) => {
        const msg = String(args[0]);
        if (msg.includes('429 rate limited')) {
          retryWarnings.push(msg);
        }
        originalWarn.apply(console, args);
      };

      const action = new GetLearnWorldsBulkDataAction();
      const startTime = Date.now();

      try {
        const result = await action.GetBulkData(
          {
            CompanyID: COMPANY_ID,
            IncludeUsers: true,
            IncludeCourses: true,
            IncludeBundles: true,
            IncludeEnrollments: true,
            IncludeProgress: true,
            IncludeCertificates: false,
            IncludeQuizResults: false,
            MaxResultsPerEntity: 20,
          },
          contextUser,
        );

        const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);

        // Log comprehensive results
        console.log(`\n  ══════════════════════════════════════════════════`);
        console.log(`  GetBulkData Rate Limit Stress Test Results`);
        console.log(`  ──────────────────────────────────────────────────`);
        console.log(`  Duration:        ${elapsed}s`);
        console.log(`  Total API calls: ${result.totalApiCalls}`);
        console.log(`  Users:           ${result.users?.length ?? 0}`);
        console.log(`  Courses:         ${result.courses?.length ?? 0}`);
        console.log(`  Bundles:         ${result.bundles?.length ?? 0}`);
        console.log(`  Enrollments:     ${result.enrollments?.length ?? 0}`);
        console.log(`  Progress:        ${result.progress?.length ?? 0}`);
        console.log(`  429 retries:     ${retryWarnings.length}`);
        console.log(`  Errors:          ${result.errors.length}`);

        if (retryWarnings.length > 0) {
          console.log(`  ── Retry details ──`);
          for (const warning of retryWarnings) {
            console.log(`    ${warning}`);
          }
        }

        if (result.errors.length > 0) {
          console.log(`  ── Errors ──`);
          for (const err of result.errors) {
            console.log(`    [${err.entity}] ${err.entityId || '(no id)'}: ${err.message}`);
          }
        }
        console.log(`  ══════════════════════════════════════════════════\n`);

        // The key assertion: zero errors means all enrollment/progress fetches succeeded.
        // Before the fix, ~35 out of 100+ users would fail with 429 errors.
        expect(result.errors).toEqual([]);
        expect(result.users).toBeDefined();
        expect(result.users!.length).toBeGreaterThan(0);
        expect(result.enrollments).toBeDefined();
        expect(result.progress).toBeDefined();

      } finally {
        console.warn = originalWarn;
      }
    }, 300_000); // 5-minute timeout — rate limiting (25 req/10s + inter-batch delays) makes bulk runs slower
  });
});
