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

/**
 * SuperSync Error Handling E2E Tests
 *
 * These tests verify the client correctly handles various error scenarios:
 * - Conflict resolution with multiple clients
 * - Concurrent modification detection
 * - Error recovery after sync failures
 */

test.describe('@supersync SuperSync Error Handling', () => {
  /**
   * Scenario: Concurrent Modification Creates Conflict
   *
   * Tests that when two clients modify the same entity concurrently,
   * the second client to sync receives a CONFLICT_CONCURRENT error
   * and handles it via LWW resolution.
   *
   * Actions:
   * 1. Client A creates Task, syncs
   * 2. Client B syncs (downloads task)
   * 3. Both clients go offline (don't sync)
   * 4. Client A modifies task title
   * 5. Client B modifies same task title (different value)
   * 6. Client A syncs first (succeeds)
   * 7. Client B syncs (gets CONFLICT_CONCURRENT, auto-resolves via LWW)
   * 8. Verify both clients converge to same state
   */
  test('Concurrent modification triggers LWW conflict resolution', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    const appUrl = baseURL || 'http://localhost:4242';
    let clientA: SimulatedE2EClient | null = null;
    let clientB: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const syncConfig = getSuperSyncConfig(user);

      // Setup clients
      clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId);
      await clientA.sync.setupSuperSync(syncConfig);

      clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId);
      await clientB.sync.setupSuperSync(syncConfig);

      // 1. Client A creates task
      const taskName = `Conflict-Test-${testRunId}`;
      await clientA.workView.addTask(taskName);
      await clientA.sync.syncAndWait();

      // 2. Client B downloads the task
      await clientB.sync.syncAndWait();
      await waitForTask(clientB.page, taskName);

      // 3-5. Both clients modify the task while "offline"
      // Client A changes title first using inline editing (dblclick)
      const taskLocatorA = clientA.page
        .locator(`task:not(.ng-animating):has-text("${taskName}")`)
        .first();
      // Wait for task to be stable before double-clicking
      await taskLocatorA.waitFor({ state: 'visible', timeout: 5000 });
      await clientA.page.waitForTimeout(500);
      await taskLocatorA.dblclick();
      const editInputA = clientA.page.locator(
        'input.mat-mdc-input-element:focus, textarea:focus',
      );
      await editInputA.waitFor({ state: 'visible', timeout: 5000 });
      await editInputA.fill(`${taskName}-ModifiedByA`);
      await editInputA.press('Enter');
      // Wait for edit to be saved
      await clientA.page.waitForTimeout(500);

      // Client B modifies the same task with a different value using inline editing
      const taskLocatorB = clientB.page
        .locator(`task:not(.ng-animating):has-text("${taskName}")`)
        .first();
      // Wait for task to be stable before double-clicking
      await taskLocatorB.waitFor({ state: 'visible', timeout: 5000 });
      await clientB.page.waitForTimeout(500);
      await taskLocatorB.dblclick();
      const editInputB = clientB.page.locator(
        'input.mat-mdc-input-element:focus, textarea:focus',
      );
      await editInputB.waitFor({ state: 'visible', timeout: 5000 });
      await editInputB.fill(`${taskName}-ModifiedByB`);
      await editInputB.press('Enter');
      // Wait for edit to be saved
      await clientB.page.waitForTimeout(500);

      // 6. Client A syncs first (succeeds)
      await clientA.sync.syncAndWait();

      // 7. Client B syncs (gets conflict, should auto-resolve via LWW)
      // The newer timestamp wins, so B's change might win or A's depending on timing
      await clientB.sync.syncAndWait();

      // Give time for any conflict resolution UI and state to settle
      await clientB.page.waitForTimeout(2000);

      // 8. Final sync to ensure convergence
      await clientA.sync.syncAndWait();
      await clientB.sync.syncAndWait();

      // Additional wait for UI to update after sync
      await clientA.page.waitForTimeout(500);
      await clientB.page.waitForTimeout(500);

      // Verify both clients have converged - they should show the same task title
      // The exact title depends on LWW resolution (whichever timestamp was later)
      // Use the task title specifically to avoid capturing other text
      const finalTaskA = await clientA.page
        .locator('task:not(.ng-animating) .task-title')
        .first()
        .textContent();
      const finalTaskB = await clientB.page
        .locator('task:not(.ng-animating) .task-title')
        .first()
        .textContent();

      // Both clients should show the same content (convergence)
      expect(finalTaskA).toBe(finalTaskB);

      console.log(
        '[Conflict-Resolution] ✓ Concurrent modification handled via LWW - clients converged',
      );
    } finally {
      if (clientA) await closeClient(clientA);
      if (clientB) await closeClient(clientB);
    }
  });

  /**
   * Scenario: Sync Recovery After Network Failure
   *
   * Tests that when sync fails due to network issues, the client
   * can recover and successfully sync on retry.
   *
   * Actions:
   * 1. Client creates task
   * 2. Client syncs successfully
   * 3. Client creates another task
   * 4. Simulate network being restored
   * 5. Client syncs again (should succeed)
   * 6. Verify all tasks are synced
   */
  test('Sync recovers after initial connection', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    const appUrl = baseURL || 'http://localhost:4242';
    let client: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const syncConfig = getSuperSyncConfig(user);

      // Setup client
      client = await createSimulatedClient(browser, appUrl, 'Recovery', testRunId);
      await client.sync.setupSuperSync(syncConfig);

      // 1. Create first task
      const task1 = `Recovery-Task1-${testRunId}`;
      await client.workView.addTask(task1);

      // 2. Sync successfully
      await client.sync.syncAndWait();

      // 3. Create second task
      const task2 = `Recovery-Task2-${testRunId}`;
      await client.workView.addTask(task2);

      // 4-5. Sync again
      await client.sync.syncAndWait();

      // 6. Verify both tasks exist
      await waitForTask(client.page, task1);
      await waitForTask(client.page, task2);

      console.log('[Sync-Recovery] ✓ Multiple syncs completed successfully');
    } finally {
      if (client) await closeClient(client);
    }
  });

  /**
   * Scenario: Duplicate Operation Handling (Idempotency)
   *
   * Tests that retrying the same operation doesn't cause issues.
   * This simulates what happens when a sync completes but the client
   * doesn't receive the response (network issue) and retries.
   *
   * Actions:
   * 1. Client creates task
   * 2. Client syncs
   * 3. Client syncs again immediately (same ops might be in queue)
   * 4. Verify no duplicate tasks created
   */
  test('Duplicate sync attempts are handled gracefully', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    const appUrl = baseURL || 'http://localhost:4242';
    let client: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const syncConfig = getSuperSyncConfig(user);

      // Setup client
      client = await createSimulatedClient(browser, appUrl, 'Idempotent', testRunId);
      await client.sync.setupSuperSync(syncConfig);

      // 1. Create task
      const taskName = `Idempotent-${testRunId}`;
      await client.workView.addTask(taskName);

      // 2-3. Sync multiple times rapidly
      await client.sync.syncAndWait();
      await client.sync.syncAndWait();
      await client.sync.syncAndWait();

      // 4. Verify exactly one task with this name exists
      const matchingTasks = client.page.locator(`task:has-text("${taskName}")`);
      const count = await matchingTasks.count();

      expect(count).toBe(1);

      console.log(
        '[Idempotency] ✓ Multiple sync attempts handled correctly - no duplicates',
      );
    } finally {
      if (client) await closeClient(client);
    }
  });

  /**
   * Scenario: Three-Client Convergence
   *
   * Tests that three clients all modifying different aspects of tasks
   * eventually converge to the same state.
   *
   * Actions:
   * 1. Client A creates Task 1
   * 2. All clients sync
   * 3. Client A creates Task 2
   * 4. Client B marks Task 1 as done
   * 5. Client C adds a note to Task 1 (if supported, else skipped)
   * 6. All clients sync
   * 7. Verify all clients have same state
   */
  test('Three clients converge to same state', async ({
    browser,
    baseURL,
    testRunId,
  }) => {
    const appUrl = baseURL || 'http://localhost:4242';
    let clientA: SimulatedE2EClient | null = null;
    let clientB: SimulatedE2EClient | null = null;
    let clientC: SimulatedE2EClient | null = null;

    try {
      const user = await createTestUser(testRunId);
      const syncConfig = getSuperSyncConfig(user);

      // Setup all three clients
      clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId);
      await clientA.sync.setupSuperSync(syncConfig);

      clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId);
      await clientB.sync.setupSuperSync(syncConfig);

      clientC = await createSimulatedClient(browser, appUrl, 'C', testRunId);
      await clientC.sync.setupSuperSync(syncConfig);

      // 1. Client A creates Task 1
      const task1 = `ThreeClient-Task1-${testRunId}`;
      await clientA.workView.addTask(task1);
      await clientA.sync.syncAndWait();

      // 2. All clients sync to get Task 1
      await clientB.sync.syncAndWait();
      await clientC.sync.syncAndWait();
      await waitForTask(clientB.page, task1);
      await waitForTask(clientC.page, task1);

      // 3. Client A creates Task 2
      const task2 = `ThreeClient-Task2-${testRunId}`;
      await clientA.workView.addTask(task2);

      // 4. Client B marks Task 1 as done
      const taskLocatorB = clientB.page
        .locator(`task:not(.ng-animating):has-text("${task1}")`)
        .first();
      await taskLocatorB.hover();
      await taskLocatorB.locator('done-toggle').click();
      await expect(taskLocatorB).toHaveClass(/isDone/);

      // 5. Client C just syncs (no modification, but will pick up B's change)

      // 6. All clients sync
      await clientA.sync.syncAndWait();
      await clientB.sync.syncAndWait();
      await clientC.sync.syncAndWait();

      // Final round to ensure convergence
      await clientA.sync.syncAndWait();
      await clientB.sync.syncAndWait();
      await clientC.sync.syncAndWait();

      // 7. Verify all clients have both tasks
      // Task 1 is done, so it may be in the "Completed Tasks" collapsible section
      // which uses @if and removes elements from the DOM when collapsed.
      // Expand it on all clients before checking.
      for (const client of [clientA!, clientB!, clientC!]) {
        const collapsedDoneSection = client.page.locator(
          'collapsible:not(.isExpanded) .collapsible-header:has-text("Completed")',
        );
        if ((await collapsedDoneSection.count()) > 0) {
          await collapsedDoneSection.click();
          await client.page.waitForTimeout(500);
        }
      }

      await waitForTask(clientA.page, task1);
      await waitForTask(clientA.page, task2);
      await waitForTask(clientB.page, task1);
      await waitForTask(clientB.page, task2);
      await waitForTask(clientC.page, task1);
      await waitForTask(clientC.page, task2);

      // Verify Task 1 is marked done on all clients
      const task1LocatorA = clientA.page
        .locator(`task:not(.ng-animating):has-text("${task1}")`)
        .first();
      const task1LocatorC = clientC.page
        .locator(`task:not(.ng-animating):has-text("${task1}")`)
        .first();

      await expect(task1LocatorA).toHaveClass(/isDone/);
      await expect(taskLocatorB).toHaveClass(/isDone/);
      await expect(task1LocatorC).toHaveClass(/isDone/);

      console.log('[Three-Client] ✓ All three clients converged to same state');
    } finally {
      if (clientA) await closeClient(clientA);
      if (clientB) await closeClient(clientB);
      if (clientC) await closeClient(clientC);
    }
  });
});
