import { test, expect } from '../../fixtures/supersync.fixture';
import {
  createTestUser,
  getSuperSyncConfig,
  createSimulatedClient,
  closeClient,
  waitForTask,
  type SimulatedE2EClient,
} from '../../utils/supersync-helpers';
import {
  expectTaskNotVisible,
  expectTaskVisible,
} from '../../utils/supersync-assertions';

interface SyncStatusResponse {
  storageUsedBytes: number;
  storageQuotaBytes: number;
}

const isSyncStatusResponse = (value: unknown): value is SyncStatusResponse => {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as SyncStatusResponse).storageUsedBytes === 'number' &&
    typeof (value as SyncStatusResponse).storageQuotaBytes === 'number'
  );
};

const getSyncStatus = async (
  baseUrl: string,
  accessToken: string,
): Promise<SyncStatusResponse> => {
  const response = await fetch(`${baseUrl}/api/sync/status`, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });

  if (!response.ok) {
    throw new Error(`Failed to get sync status: ${response.status}`);
  }

  const body: unknown = await response.json();
  if (!isSyncStatusResponse(body)) {
    throw new Error(`Unexpected sync status response: ${JSON.stringify(body)}`);
  }

  return body;
};

const useServerDataIfPrompted = async (client: SimulatedE2EClient): Promise<void> => {
  const appeared = await client.sync.syncImportConflictDialog
    .waitFor({ state: 'visible', timeout: 15000 })
    .then(() => true)
    .catch(() => false);

  if (!appeared) return;

  await client.sync.syncImportUseRemoteBtn.click();
  await client.sync.syncImportConflictDialog.waitFor({
    state: 'hidden',
    timeout: 5000,
  });
};

const recoverWithNewPasswordAndServerData = async (
  client: SimulatedE2EClient,
  newPassword: string,
): Promise<void> => {
  const decryptErrorDialog = client.page.locator('dialog-handle-decrypt-error');

  const dialogAlreadyVisible = await decryptErrorDialog.isVisible().catch(() => false);
  if (!dialogAlreadyVisible) {
    await client.sync.syncBtn.click();
  }

  const decryptAppeared = await decryptErrorDialog
    .waitFor({ state: 'visible', timeout: 15000 })
    .then(() => true)
    .catch(() => false);

  if (decryptAppeared) {
    const passwordInput = decryptErrorDialog.locator('input[type="password"]');
    await passwordInput.waitFor({ state: 'visible', timeout: 5000 });
    await passwordInput.fill(newPassword);

    await decryptErrorDialog.locator('button:has-text("Retry Decrypt")').first().click();
    await decryptErrorDialog.waitFor({ state: 'hidden', timeout: 10000 });
  }

  await useServerDataIfPrompted(client);
  await client.sync.syncSpinner.waitFor({ state: 'hidden', timeout: 30000 });
  await client.sync.syncAndWait();
  await useServerDataIfPrompted(client);
};

/**
 * SuperSync Encryption Password Change E2E Tests
 *
 * Verifies the encryption password change flow:
 * - Password change deletes server data and re-uploads with new password
 * - Existing data is preserved after password change
 * - Other clients must use the new password to sync
 * - Old password no longer works after change
 *
 * Run with E2E_VERBOSE=1 to see browser console logs for debugging.
 */

test.describe('@supersync SuperSync Encryption Password Change', () => {
  test('Password change preserves existing data', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    let clientA: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const baseConfig = getSuperSyncConfig(user);
      const oldPassword = `oldpass-${testRunId}`;
      const newPassword = `newpass-${testRunId}`;

      // --- Setup with initial password ---
      clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
      await clientA.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword,
      });

      // Create tasks (names must be distinct - no substring overlap)
      const task1 = `TaskA-${testRunId}`;
      const task2 = `TaskB-${testRunId}`;
      await clientA.workView.addTask(task1);
      await clientA.page.waitForTimeout(100);
      await clientA.workView.addTask(task2);

      // Sync with old password
      await clientA.sync.syncAndWait();

      // Verify tasks exist
      await waitForTask(clientA.page, task1);
      await waitForTask(clientA.page, task2);

      // --- Change password ---
      await clientA.sync.changeEncryptionPassword(newPassword);

      // --- Verify tasks still exist after password change ---
      await expectTaskVisible(clientA, task1);
      await expectTaskVisible(clientA, task2);

      // Trigger another sync to verify everything works
      await clientA.sync.syncAndWait();

      // Tasks should still be there
      await expectTaskVisible(clientA, task1);
      await expectTaskVisible(clientA, task2);
    } finally {
      if (clientA) await closeClient(clientA);
    }
  });

  test('New client can sync with new password after password change', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    let clientA: SimulatedE2EClient | null = null;
    let clientB: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const baseConfig = getSuperSyncConfig(user);
      const oldPassword = `oldpass-${testRunId}`;
      const newPassword = `newpass-${testRunId}`;

      // --- Client A: Setup and create data ---
      clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
      await clientA.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword,
      });

      const taskName = `BeforeChange-${testRunId}`;
      await clientA.workView.addTask(taskName);
      await clientA.sync.syncAndWait();

      // --- Client A: Change password ---
      await clientA.sync.changeEncryptionPassword(newPassword);

      // --- Client B: Setup with NEW password ---
      clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
      await clientB.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: newPassword,
      });

      // Sync should succeed with new password
      await clientB.sync.syncAndWait();

      // Verify task synced to Client B
      await waitForTask(clientB.page, taskName);
      await expectTaskVisible(clientB, taskName);
    } finally {
      if (clientA) await closeClient(clientA);
      if (clientB) await closeClient(clientB);
    }
  });

  test('Old password fails after password change', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    let clientA: SimulatedE2EClient | null = null;
    let clientC: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const baseConfig = getSuperSyncConfig(user);
      const oldPassword = `oldpass-${testRunId}`;
      const newPassword = `newpass-${testRunId}`;

      // --- Client A: Setup, create data, and change password ---
      clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
      await clientA.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword,
      });

      const taskName = `SecretTask-${testRunId}`;
      await clientA.workView.addTask(taskName);
      await clientA.sync.syncAndWait();

      // Change password
      await clientA.sync.changeEncryptionPassword(newPassword);

      // --- Client C: Try to sync with OLD password ---
      clientC = await createSimulatedClient(browser, baseURL!, 'C', testRunId);
      // Use waitForInitialSync: false because the sync will fail with a decrypt error
      // The sync is triggered automatically after saving the config
      await clientC.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword, // Using OLD password!
        waitForInitialSync: false, // Don't wait for initial sync - it will fail
      });

      // The sync is triggered automatically after setupSuperSync saves the config.
      // Wait for the decrypt error dialog to appear (it should show automatically)
      const decryptErrorDialog = clientC.page.locator('dialog-handle-decrypt-error');
      const dialogAppeared = await decryptErrorDialog
        .waitFor({ state: 'visible', timeout: 15000 })
        .then(() => true)
        .catch(() => false);

      if (dialogAppeared) {
        console.log('Decrypt error dialog appeared as expected');
        // Close the dialog
        const cancelBtn = decryptErrorDialog
          .locator('button')
          .filter({ hasText: /cancel/i });
        if (await cancelBtn.isVisible()) {
          await cancelBtn.click();
          await decryptErrorDialog.waitFor({ state: 'hidden', timeout: 5000 });
        }
      }

      // Verify Client C does NOT have the task
      await expect(
        clientC.page.locator(`task:has-text("${taskName}")`),
      ).not.toBeVisible();

      // Check for error state
      const hasError = await clientC.sync.hasSyncError();
      // App uses snack-custom component, not simple-snack-bar
      const snackbar = clientC.page.locator(
        'snack-custom:has-text("decrypt"), ' +
          'snack-custom:has-text("password"), ' +
          '.mat-mdc-snack-bar-container:has-text("decrypt"), ' +
          '.mat-mdc-snack-bar-container:has-text("password")',
      );
      const snackbarVisible = await snackbar
        .first()
        .isVisible()
        .catch(() => false);

      // Either decrypt error dialog appeared, error icon, or error snackbar should be visible.
      // The decrypt error dialog is the primary indicator that the old password failed.
      expect(dialogAppeared || hasError || snackbarVisible).toBe(true);
    } finally {
      if (clientA) await closeClient(clientA);
      if (clientC) await closeClient(clientC);
    }
  });

  test('Incompatible stale client adopts Client A clean slate and drops pending data', async ({
    browser,
    baseURL,
    testRunId,
  }, testInfo) => {
    testInfo.setTimeout(180000);
    const appUrl = baseURL || 'http://localhost:4242';
    let clientA: SimulatedE2EClient | null = null;
    let clientB: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const baseConfig = getSuperSyncConfig(user);
      const oldPassword = `oldpass-${testRunId}`;
      const newPassword = `newpass-${testRunId}`;

      clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId);
      await clientA.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword,
      });

      clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId);
      await clientB.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword,
      });

      const taskFromA = `A-Compatible-${testRunId}`;
      await clientA.workView.addTask(taskFromA);
      await clientA.sync.syncAndWait();

      await clientB.sync.syncAndWait();
      await waitForTask(clientB.page, taskFromA);

      const staleTaskFromB = `B-Stale-${testRunId}`;
      await clientB.workView.addTask(staleTaskFromB);
      await waitForTask(clientB.page, staleTaskFromB);

      const passwordChange = clientA.sync.changeEncryptionPassword(newPassword);
      await clientB.sync.syncBtn.click();
      await clientB.sync.syncSpinner
        .waitFor({ state: 'hidden', timeout: 30000 })
        .catch(() => undefined);
      await passwordChange;

      const taskFromAAfterChange = `A-AfterChange-${testRunId}`;
      await clientA.workView.addTask(taskFromAAfterChange);
      await clientA.sync.syncAndWait();

      await recoverWithNewPasswordAndServerData(clientB, newPassword);

      await waitForTask(clientB.page, taskFromA);
      await waitForTask(clientB.page, taskFromAAfterChange);
      await expectTaskVisible(clientA, taskFromA);
      await expectTaskVisible(clientA, taskFromAAfterChange);
      await expectTaskVisible(clientB, taskFromA);
      await expectTaskVisible(clientB, taskFromAAfterChange);
      await expectTaskNotVisible(clientA, staleTaskFromB, 5000);
      await expectTaskNotVisible(clientB, staleTaskFromB, 5000);

      await expect(clientB.page.locator('dialog-handle-decrypt-error')).not.toBeVisible();
      await expect(clientB.sync.syncImportConflictDialog).not.toBeVisible();

      const status = await getSyncStatus(baseConfig.baseUrl, baseConfig.accessToken);
      expect(status.storageUsedBytes).toBeLessThanOrEqual(status.storageQuotaBytes);

      const taskFromBAfterRecovery = `B-Recovered-${testRunId}`;
      await clientB.workView.addTask(taskFromBAfterRecovery);
      await clientB.sync.syncAndWait();

      await clientA.sync.syncAndWait();
      await waitForTask(clientA.page, taskFromBAfterRecovery);
      await expectTaskVisible(clientA, taskFromBAfterRecovery);
    } finally {
      if (clientA) await closeClient(clientA);
      if (clientB) await closeClient(clientB);
    }
  });

  test('Bidirectional sync works after password change', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    let clientA: SimulatedE2EClient | null = null;
    let clientB: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const baseConfig = getSuperSyncConfig(user);
      const oldPassword = `oldpass-${testRunId}`;
      const newPassword = `newpass-${testRunId}`;

      // --- Setup both clients with old password ---
      clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
      await clientA.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword,
      });

      clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
      await clientB.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword,
      });

      // --- Create initial tasks and sync ---
      const taskFromA = `FromA-${testRunId}`;
      await clientA.workView.addTask(taskFromA);
      await clientA.sync.syncAndWait();

      await clientB.sync.syncAndWait();
      await waitForTask(clientB.page, taskFromA);

      // --- Client A changes password ---
      await clientA.sync.changeEncryptionPassword(newPassword);

      // --- Client B must reconfigure with new password ---
      // Close and recreate with new password (simulating user entering new password)
      await closeClient(clientB);
      clientB = await createSimulatedClient(browser, baseURL!, 'B2', testRunId);
      await clientB.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: newPassword,
      });
      await clientB.sync.syncAndWait();

      // Verify B has the task
      await waitForTask(clientB.page, taskFromA);

      // --- Client B creates a new task ---
      const taskFromB = `FromB-${testRunId}`;
      await clientB.workView.addTask(taskFromB);
      await clientB.sync.syncAndWait();

      // --- Client A syncs and should get B's task ---
      await clientA.sync.syncAndWait();
      await waitForTask(clientA.page, taskFromB);

      // Verify both clients have both tasks
      await expectTaskVisible(clientA, taskFromA);
      await expectTaskVisible(clientA, taskFromB);
      await expectTaskVisible(clientB, taskFromA);
      await expectTaskVisible(clientB, taskFromB);
    } finally {
      if (clientA) await closeClient(clientA);
      if (clientB) await closeClient(clientB);
    }
  });

  test('Multiple password changes work correctly', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    let clientA: SimulatedE2EClient | null = null;
    let clientB: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const baseConfig = getSuperSyncConfig(user);
      const password1 = `pass1-${testRunId}`;
      const password2 = `pass2-${testRunId}`;
      const password3 = `pass3-${testRunId}`;

      // --- Setup with password1 ---
      clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
      await clientA.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: password1,
      });

      // Create and sync task1
      const task1 = `Task1-${testRunId}`;
      await clientA.workView.addTask(task1);
      await clientA.sync.syncAndWait();

      // --- Change to password2 ---
      await clientA.sync.changeEncryptionPassword(password2);

      // Create and sync task2
      const task2 = `Task2-${testRunId}`;
      await clientA.workView.addTask(task2);
      await clientA.sync.syncAndWait();

      // --- Change to password3 ---
      await clientA.sync.changeEncryptionPassword(password3);

      // Create and sync task3
      const task3 = `Task3-${testRunId}`;
      await clientA.workView.addTask(task3);
      await clientA.sync.syncAndWait();

      // --- Verify all tasks still exist ---
      await expectTaskVisible(clientA, task1);
      await expectTaskVisible(clientA, task2);
      await expectTaskVisible(clientA, task3);

      // --- New client with password3 should see all tasks ---
      clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
      await clientB.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: password3,
      });
      await clientB.sync.syncAndWait();

      await waitForTask(clientB.page, task1);
      await waitForTask(clientB.page, task2);
      await waitForTask(clientB.page, task3);

      await expectTaskVisible(clientB, task1);
      await expectTaskVisible(clientB, task2);
      await expectTaskVisible(clientB, task3);
    } finally {
      if (clientA) await closeClient(clientA);
      if (clientB) await closeClient(clientB);
    }
  });

  test('Error dialog recovery: entering new password after change works', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    let clientA: SimulatedE2EClient | null = null;
    let clientB: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const baseConfig = getSuperSyncConfig(user);
      const oldPassword = `oldpass-${testRunId}`;
      const newPassword = `newpass-${testRunId}`;

      // --- Setup both clients with old password ---
      clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
      await clientA.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword,
      });

      clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
      await clientB.sync.setupSuperSync({
        ...baseConfig,
        isEncryptionEnabled: true,
        password: oldPassword,
      });

      // --- Create task and sync on both clients ---
      const taskBeforeChange = `BeforeChange-${testRunId}`;
      await clientA.workView.addTask(taskBeforeChange);
      await clientA.sync.syncAndWait();
      await clientB.sync.syncAndWait();
      await waitForTask(clientB.page, taskBeforeChange);

      // --- Client A changes password (triggers clean slate) ---
      await clientA.sync.changeEncryptionPassword(newPassword);

      // Create new task after password change
      const taskAfterChange = `AfterChange-${testRunId}`;
      await clientA.workView.addTask(taskAfterChange);
      await clientA.sync.syncAndWait();

      // --- Client B tries to sync with OLD password ---
      // This will:
      // 1. Detect gap (lastServerSeq > latestSeq due to clean slate)
      // 2. Reset and re-download from seq 0
      // 3. Download ops encrypted with NEW password
      // 4. Try to decrypt with OLD password → DecryptError
      // 5. Show decrypt error dialog

      // Click sync button directly — triggerSync() throws on error state before
      // the async decrypt error dialog has time to render
      await clientB.sync.syncBtn.click();

      // Wait for decrypt error dialog to appear
      // Use specific component selector to avoid strict mode violation
      const decryptErrorDialog = clientB.page.locator('dialog-handle-decrypt-error');
      await decryptErrorDialog.waitFor({ state: 'visible', timeout: 10000 });

      // --- Enter NEW password in the error dialog ---
      const passwordInput = decryptErrorDialog.locator('input[type="password"]');
      await passwordInput.waitFor({ state: 'visible', timeout: 5000 });
      await passwordInput.fill(newPassword);

      // Click "Update Password & Resync" button
      const updateButton = decryptErrorDialog.locator('button:has-text("Retry Decrypt")');
      await updateButton.first().click();

      // Wait for dialog to close
      await decryptErrorDialog.waitFor({ state: 'hidden', timeout: 5000 });

      // --- Verify sync completes successfully with new password ---
      // After retry, the sync discovers the clean slate (SYNC_IMPORT) from the
      // password change and may show a sync-import-conflict dialog. Handle it.
      const syncImportDialog = clientB.page.locator('dialog-sync-import-conflict');
      const syncImportAppeared = await syncImportDialog
        .waitFor({ state: 'visible', timeout: 15000 })
        .then(() => true)
        .catch(() => false);

      if (syncImportAppeared) {
        console.log(
          '[PasswordChange] Sync import conflict dialog appeared - using server data',
        );
        await clientB.page
          .locator('dialog-sync-import-conflict button:has-text("Use Server Data")')
          .click();
        await syncImportDialog.waitFor({ state: 'hidden', timeout: 5000 });
      }

      // Now wait for sync to complete (may need another sync round)
      await clientB.sync.syncAndWait();

      // Verify Client B received the task created after password change
      await waitForTask(clientB.page, taskAfterChange);
      await expectTaskVisible(clientB, taskAfterChange);

      // Also verify the old task is still there
      await expectTaskVisible(clientB, taskBeforeChange);

      // --- Verify bidirectional sync works after recovery ---
      const taskFromB = `FromB-${testRunId}`;
      await clientB.workView.addTask(taskFromB);
      await clientB.sync.syncAndWait();

      await clientA.sync.syncAndWait();
      await waitForTask(clientA.page, taskFromB);
      await expectTaskVisible(clientA, taskFromB);
    } finally {
      if (clientA) await closeClient(clientA);
      if (clientB) await closeClient(clientB);
    }
  });
});
