import type { ProcessActions } from '../process-actions.js';
import { test, expect } from '../../fixtures.js';
import { convexRun } from '../../helpers/convex.js';

const DIALOG = '.p-dialog-mask .p-dialog';

/**
 * Imports for a process: page renders, import button opens file dialog,
 * data table shows existing imports.
 *
 * URL contract:
 *  - NestJS processes (as, me, asa, ...): `/admin/applications/<p>/imports`
 *  - Convex processes (bwl, ...): `/admin/applications/<p>/manage/imports`
 *
 * Both shapes are accepted; the test navigates to the legacy URL and
 * follows the redirect / matches whichever the app routes to.
 *
 * Skip is handled centrally by `defineProcessTestSuite`.
 */
export function registerImportsSpec(actions: ProcessActions): void {
  // Convex processes route through `/manage/imports`; legacy NestJS
  // through `/imports`. Backend choice lives on the registered
  // `ProcessActions.isConvex` flag so a new Convex process automatically
  // routes correctly the moment it registers with `isConvex: true`.
  const importsPath = actions.isConvex
    ? `/admin/applications/${actions.process}/manage/imports`
    : `/admin/applications/${actions.process}/imports`;
  const importsUrlRegex = actions.isConvex
    ? new RegExp(`/applications/${actions.process}/manage/imports`)
    : new RegExp(`/applications/${actions.process}/imports`);

  test.describe(`Imports for ${actions.process}`, () => {
    test.beforeEach(({ customerConfig }) => {
      test.skip(
        customerConfig.name !== actions.customer,
        `Suite for ${actions.process} only runs against ${actions.customer} (got ${customerConfig.name})`
      );
    });

    test('imports page loads with data table', async ({ adminPage }) => {
      await adminPage.goto(importsPath);
      await expect(adminPage).toHaveURL(importsUrlRegex);
      await expect(adminPage.locator('p-card').first()).toBeVisible({
        timeout: 10_000,
      });
      await expect(
        adminPage.locator('masterev-infinite-data-table')
      ).toBeVisible({ timeout: 10_000 });
    });

    test('import button is visible in header', async ({ adminPage }) => {
      await adminPage.goto(importsPath);
      await expect(
        adminPage.locator('masterev-infinite-data-table')
      ).toBeVisible({ timeout: 10_000 });
      const importButton = adminPage.locator('[data-testid="import-button"]');
      await expect(importButton).toBeVisible({ timeout: 5_000 });
    });

    test('import button opens file select dialog', async ({ adminPage }) => {
      await adminPage.goto(importsPath);
      await expect(
        adminPage.locator('masterev-infinite-data-table')
      ).toBeVisible({ timeout: 10_000 });
      await adminPage.locator('[data-testid="import-button"]').click();
      const dialog = adminPage.locator(DIALOG).first();
      await expect(dialog).toBeVisible({ timeout: 5_000 });
      await expect(dialog.locator('masterev-file-dropzone')).toBeVisible({
        timeout: 5_000,
      });
      await adminPage.keyboard.press('Escape');
      await expect(dialog).not.toBeVisible({ timeout: 5_000 });
    });

    test('search input filters imports', async ({ adminPage }) => {
      await adminPage.goto(importsPath);
      const table = adminPage.locator('masterev-infinite-data-table');
      await expect(table).toBeVisible({ timeout: 10_000 });
      const searchInput = adminPage
        .locator('masterev-search-input input, input[type="search"]')
        .first();
      await expect(searchInput).toBeVisible({ timeout: 5_000 });
      await searchInput.fill('test');
      await adminPage.waitForTimeout(1_000);
      await searchInput.fill('');
    });

    // ============================================================
    // Convex-only: Manage-Section tabs + period auto-creation flow
    // ============================================================
    if (actions.isConvex) {
      test('manage section redirects /manage to /manage/imports', async ({
        adminPage,
      }) => {
        await adminPage.goto(`/admin/applications/${actions.process}/manage`);
        await expect(adminPage).toHaveURL(
          new RegExp(`/applications/${actions.process}/manage/imports`),
          { timeout: 10_000 }
        );
      });

      test('manage section has three tabs (imports/periods/historic)', async ({
        adminPage,
      }) => {
        await adminPage.goto(importsPath);
        await expect(adminPage.locator('masterev-tabs')).toBeVisible({
          timeout: 10_000,
        });
        await expect(adminPage.locator('masterev-tablist')).toBeVisible();
        const tabs = adminPage.locator('masterev-tab');
        await expect(tabs).toHaveCount(3);
      });

      test('periods tab loads', async ({ adminPage }) => {
        await adminPage.goto(
          `/admin/applications/${actions.process}/manage/periods`
        );
        await expect(adminPage).toHaveURL(
          new RegExp(`/applications/${actions.process}/manage/periods`),
          { timeout: 10_000 }
        );
        // Period table is rendered by `masterev-application-periods-tab`.
        await expect(
          adminPage.locator('masterev-application-periods-tab')
        ).toBeVisible({ timeout: 10_000 });
      });

      test('historic tab loads', async ({ adminPage }) => {
        await adminPage.goto(
          `/admin/applications/${actions.process}/manage/historic`
        );
        await expect(adminPage).toHaveURL(
          new RegExp(`/applications/${actions.process}/manage/historic`),
          { timeout: 10_000 }
        );
        await expect(
          adminPage.locator('masterev-application-historic-tab')
        ).toBeVisible({ timeout: 10_000 });
      });

      // ==========================================================
      // Generate-fakes dialog: real application periods, empty
      // states, CSV download flow.
      //
      // The prerequisites (>=1 applicationPeriod, >=1 country) are
      // seeded idempotently via `e2e:ensureFakesPrerequisites`. On
      // staging the BMT customer can land in a no-periods state after
      // a semester reset, which would otherwise make these tests
      // assert against the empty-state error instead of the select.
      // ==========================================================
      const fakesPeriodShort = 'E2EFAKES';

      test('generate-fakes dialog opens with real application periods', async ({
        adminPage,
      }) => {
        await convexRun('e2e:ensureFakesPrerequisites', {
          customer: actions.customer,
          process: actions.process,
          periodShort: fakesPeriodShort,
        });

        await adminPage.goto(importsPath);
        await expect(
          adminPage.locator('masterev-infinite-data-table')
        ).toBeVisible({ timeout: 10_000 });

        await adminPage
          .locator('[data-testid="generate-fakes-button"]')
          .click();

        const dialog = adminPage.locator(DIALOG).first();
        await expect(dialog).toBeVisible({ timeout: 5_000 });
        await expect(
          dialog.locator('masterev-data-imports-generate-fakes-dialog')
        ).toBeVisible({ timeout: 5_000 });

        // Period select (Convex path) must be visible — neither empty
        // state should be active in the BMT test customer.
        const periodSelect = dialog.locator(
          '[data-testid="fakes-period-select"]'
        );
        await expect(periodSelect).toBeVisible({ timeout: 5_000 });
        await expect(
          dialog.locator('[data-testid="fakes-no-periods-error"]')
        ).toHaveCount(0);
        await expect(
          dialog.locator('[data-testid="fakes-no-countries-error"]')
        ).toHaveCount(0);

        // The selected option carries the period-tag component (same
        // visual language as the header period switcher). The tag is
        // rendered inside the p-select's "selectedItem" slot.
        await expect(
          periodSelect.locator('masterev-application-period-tag').first()
        ).toBeVisible({ timeout: 5_000 });

        // Open the dropdown panel and assert at least one option is
        // rendered with a period tag. `appendTo="body"` puts the panel
        // outside the dialog, so locate it on `adminPage`.
        await periodSelect.locator('p-select').click();
        const panel = adminPage.locator('.p-select-overlay');
        await expect(panel).toBeVisible({ timeout: 5_000 });
        await expect(
          panel.locator('masterev-application-period-tag').first()
        ).toBeVisible({ timeout: 5_000 });
        // Close the panel before cancelling so PrimeNG cleans up the
        // overlay before we assert the dialog disappears.
        await adminPage.keyboard.press('Escape');

        // Both action buttons are reachable and enabled (countries +
        // periods present).
        const importBtn = dialog.locator('[data-testid="fakes-import-button"]');
        const downloadBtn = dialog.locator(
          '[data-testid="fakes-download-button"]'
        );
        await expect(importBtn).toBeEnabled();
        await expect(downloadBtn).toBeEnabled();

        await dialog.locator('[data-testid="fakes-cancel-button"]').click();
        await expect(dialog).not.toBeVisible({ timeout: 5_000 });
      });

      test('generate-fakes "Download CSV" delivers a CSV with rows for the selected period', async ({
        adminPage,
      }) => {
        await convexRun('e2e:ensureFakesPrerequisites', {
          customer: actions.customer,
          process: actions.process,
          periodShort: fakesPeriodShort,
        });

        await adminPage.goto(importsPath);
        await expect(
          adminPage.locator('masterev-infinite-data-table')
        ).toBeVisible({ timeout: 10_000 });

        await adminPage
          .locator('[data-testid="generate-fakes-button"]')
          .click();
        const dialog = adminPage.locator(DIALOG).first();
        await expect(dialog).toBeVisible({ timeout: 5_000 });
        await expect(
          dialog.locator('[data-testid="fakes-period-select"]')
        ).toBeVisible({ timeout: 5_000 });

        // Read the period code from the selected-item period tag. The
        // CSV contains this as a row value (bewerbungszeitraum column);
        // the dialog passes `period.short` straight through to the
        // generateFakes action.
        const selectedShort = (
          await dialog
            .locator(
              '[data-testid="fakes-period-select"] masterev-application-period-tag'
            )
            .first()
            .innerText()
        ).trim();
        expect(selectedShort.length).toBeGreaterThan(0);

        const downloadPromise = adminPage.waitForEvent('download', {
          timeout: 30_000,
        });
        await dialog.locator('[data-testid="fakes-download-button"]').click();
        const download = await downloadPromise;

        // The backend's `fakeCsvFilename` shape is
        // `CAMPUSonline_<customer>_<process>_fakes.csv` and the period
        // is encoded inside rows, not the filename. Assert the shape
        // here so a future filename change is caught explicitly.
        const filename = download.suggestedFilename();
        expect(filename).toMatch(
          new RegExp(`^CAMPUSonline_.*${actions.process}.*_fakes\\.csv$`, 'i')
        );

        // Read the CSV body and assert the selected period appears in
        // at least one data row. Streaming-read via the download stream
        // keeps the file under control across all customers.
        const stream = await download.createReadStream();
        const chunks: Buffer[] = [];
        await new Promise<void>((resolve, reject) => {
          stream.on('data', (c) => chunks.push(Buffer.from(c)));
          stream.on('end', () => resolve());
          stream.on('error', reject);
        });
        const csv = Buffer.concat(chunks).toString('utf8');
        expect(csv.length).toBeGreaterThan(0);
        expect(csv).toContain(selectedShort);

        // Dialog closes itself on success.
        await expect(dialog).not.toBeVisible({ timeout: 10_000 });
      });

      // ==========================================================
      // Revert dialog: opens with filename, cancel is non-destructive.
      // The end-to-end revert (generate -> revert -> verify gone) is
      // intentionally NOT here — it would mutate shared dev state and
      // race with parallel workers. The chunked-revert mutation itself
      // is covered by `apps/backend/convex-test/applicationImports.test.ts`.
      // ==========================================================
      test('revert dialog opens for an existing import and cancel keeps the row', async ({
        adminPage,
      }) => {
        await adminPage.goto(importsPath);
        const table = adminPage.locator('masterev-infinite-data-table');
        await expect(table).toBeVisible({ timeout: 10_000 });

        const revertButtons = adminPage.locator(
          '[data-testid="revert-button"]'
        );
        const count = await revertButtons.count();
        test.skip(
          count === 0,
          'No existing import rows to revert against — seed BMT fakes first'
        );

        // Snapshot the current row count via the revert-button locator
        // (1:1 with imports) so we can assert "nothing was deleted"
        // after cancelling.
        const before = count;
        await revertButtons.first().click();

        const dialog = adminPage.locator(DIALOG).first();
        await expect(dialog).toBeVisible({ timeout: 5_000 });
        await expect(
          dialog.locator('masterev-data-imports-revert-dialog')
        ).toBeVisible({ timeout: 5_000 });
        await expect(
          dialog.locator('[data-testid="revert-idle"]')
        ).toBeVisible();
        // Filename slot is populated from the row data.
        await expect(
          dialog.locator('[data-testid="revert-filename"]')
        ).not.toBeEmpty();
        await expect(
          dialog.locator('[data-testid="revert-confirm-button"]')
        ).toBeEnabled();

        await dialog.locator('[data-testid="revert-cancel-button"]').click();
        await expect(dialog).not.toBeVisible({ timeout: 5_000 });

        // The row must still be there — cancel is non-destructive.
        await expect(revertButtons).toHaveCount(before);
      });
    }
  });
}
