import type { Page } from '@playwright/test';
import * as apps from '../helpers/applications.js';
import { convexRun } from '../helpers/convex.js';
import type {
  CreatedApplication,
  ProcessActions,
  SeedOptions,
} from '../framework/process-actions.js';

/**
 * BMT-specific E2E actions on top of the generic helpers in
 * `apps/e2e/src/helpers/applications.ts`. Tests call these so the
 * spec reads at the granularity of the FPSO process, not the
 * underlying field-key plumbing.
 *
 * Field-key contract mirrors the backend (see
 * `apps/backend/convex/lib/processes/bwl.ts` step keys + the input
 * schemas in
 * `apps/tum-som-masterev/.../convex/bwl-step-schemas.ts`).
 */

const PROCESS = 'bwl';
const CUSTOMER = 'tum-som';

export interface BwlSeedOptions extends SeedOptions {}

/** Create a BMT application and return the doc id + applicationId. */
export async function seedApplication(
  opts: BwlSeedOptions = {}
): Promise<apps.CreatedApplication> {
  // BMT's `s1.textevaluation2.onCalculate` reads
  // `meta.SV_ZEUGNIS_DURCHSCHNITT` via `readGradeStrict` once `h.complete`
  // is true (i.e. when `completeStep` runs). Without it the step
  // mutation throws and `step.result` stays `'new'`. Seed a default
  // German grade for tests that don't override it.
  const meta = {
    SV_ZEUGNIS_DURCHSCHNITT: opts.grade ?? '1,3',
    ...opts.meta,
  };
  return await apps.createApplication({
    customer: CUSTOMER,
    process: PROCESS,
    stammdaten: opts.stammdaten,
    meta,
    started: opts.started,
    applicantId: opts.applicantId,
  });
}

export async function fillTestresults(
  page: Page,
  args: { mathLogic: number; mint: number }
): Promise<void> {
  await apps.fillReactiveField(
    page,
    's1.testresults.mathLogic',
    args.mathLogic
  );
  await apps.fillReactiveField(page, 's1.testresults.mint', args.mint);
}

export async function fillTextEvaluation(
  page: Page,
  variant: 1 | 2,
  args: {
    comp: number;
    con: number;
    lang: number;
    evaluator: string;
    protocol?: string;
  }
): Promise<void> {
  const step = variant === 1 ? 'textevaluation1' : 'textevaluation2';
  await apps.selectStepByLabel(
    page,
    variant === 1 ? 'Text bewerten (1)' : 'Text bewerten (2)'
  );
  await apps.fillReactiveField(page, `s1.${step}.comp`, args.comp);
  await apps.fillReactiveField(page, `s1.${step}.con`, args.con);
  await apps.fillReactiveField(page, `s1.${step}.lang`, args.lang);
  await apps.fillReactiveField(page, `s1.${step}.evaluator`, args.evaluator);
  if (args.protocol !== undefined) {
    await apps.fillReactiveField(page, `s1.${step}.protocol`, args.protocol);
  }
}

/**
 * Mark the export step as exported. Picks today's calendar cell
 * because typing into PrimeNG `p-datepicker` is locale-fragile and
 * no spec asserts on the persisted date.
 *
 * Idempotent: when the field is already saved (status `ok`, e.g.
 * because the backend `defaultData` hook on `s2.export` prefilled
 * today's date when the step doc was created), we skip the date-
 * picker click entirely. Re-picking the same date triggers a no-op
 * `upsertField` whose `applicationQuery` snapshot is referentially
 * identical to the previous one, which leaves the detail page's
 * `previewDirty` signal stuck at `true` and disables
 * "Schritt abschließen". Skipping the click avoids that trap and
 * lets the helper work both for fresh apps (legacy path, no
 * `defaultData`) and for the new prefilled path.
 */
export async function markExported(page: Page): Promise<void> {
  // Sidebar label comes from the i18n key
  // `process.bwl.stages.s2.steps.export.label` ("TUMonline verbuchen"),
  // not from the engine-side `step.label` field in `bwl.ts` which
  // carries the longer "TUMonline verbuchen / Bewertung abgeschlossen".
  await apps.selectStepByLabel(page, 'TUMonline verbuchen');
  const field = page.locator(
    'masterev-reactive-field[data-field-key="s2.export.exportedAt"]'
  );
  const status = await field.getAttribute('data-field-status');
  if (status === 'ok') {
    return;
  }
  await apps.fillReactiveDateField(page, 's2.export.exportedAt');
}

/**
 * Drive the full happy-path stage 1 evaluation for a freshly imported
 * BMT application: testresults -> textevaluation1 -> textevaluation2.
 * Both evaluators score above the FPSO threshold.
 */
export async function runStage1Accept(page: Page): Promise<void> {
  await fillTestresults(page, { mathLogic: 28, mint: 26 });
  await apps.completeStep(page, 's1', 'testresults');

  await fillTextEvaluation(page, 1, {
    comp: 4,
    con: 3,
    lang: 3,
    evaluator: 'E2E-1',
  });
  await apps.completeStep(page, 's1', 'textevaluation1');

  await fillTextEvaluation(page, 2, {
    comp: 4,
    con: 3,
    lang: 3,
    evaluator: 'E2E-2',
  });
  await apps.completeStep(page, 's1', 'textevaluation2');
}

/**
 * Drive a stage-1 evaluation that lands below the FPSO threshold. Both
 * evaluators score 0 across the board so the calc rejects.
 */
export async function runStage1Reject(page: Page): Promise<void> {
  await fillTestresults(page, { mathLogic: 5, mint: 5 });
  await apps.completeStep(page, 's1', 'testresults');

  await fillTextEvaluation(page, 1, {
    comp: 0,
    con: 0,
    lang: 0,
    evaluator: 'E2E-1',
  });
  await apps.completeStep(page, 's1', 'textevaluation1');

  await fillTextEvaluation(page, 2, {
    comp: 0,
    con: 0,
    lang: 0,
    evaluator: 'E2E-2',
  });
  await apps.completeStep(page, 's1', 'textevaluation2');
}

async function driveToCompletedStage1(
  page: Page,
  app: CreatedApplication,
  decision: 'accept' | 'reject'
): Promise<{ decision: 'accept' | 'reject' }> {
  await apps.gotoApplicationDetail(page, PROCESS, app._id);
  if (decision === 'accept') {
    await runStage1Accept(page);
  } else {
    await runStage1Reject(page);
  }
  return { decision };
}

/**
 * Drive the stage-2 administrative step `s2.export` (TUMonline-
 * Verbuchung) to completion. Required for BMT to reach a final
 * application status — with ADR-0018 Update 2026-05-11 in place the
 * engine keeps the application `in_progress` after Stage 1 even when
 * the decision is set, and only finalises (`accepted` / `rejected`)
 * once every required step is `done`. This function is therefore the
 * step that flips the status into the terminal value.
 */
async function driveToFinalStage(
  page: Page,
  app: CreatedApplication
): Promise<void> {
  await apps.gotoApplicationDetail(page, PROCESS, app._id);
  await markExported(page);
  await apps.completeStep(page, 's2', 'export');
}

/**
 * Implementation of the `ProcessActions` interface for the BMT pilot
 * (tum-som / bwl). New processes mirror this shape — the fact that
 * this is a typed value (not a loose namespace) is what guarantees
 * the suite-builder catches missing methods at compile time.
 */
export const bwlActions: ProcessActions = {
  customer: CUSTOMER,
  process: PROCESS,
  isConvex: true,

  seedApplication,

  driveToCompletedStage1: (page, app) =>
    driveToCompletedStage1(page, app, 'accept'),

  driveToFinalStage,

  driveToRejectableState: async (page, app) => {
    await driveToCompletedStage1(page, app, 'reject');
  },

  driveToAcceptableState: async (page, app) => {
    await driveToCompletedStage1(page, app, 'accept');
  },

  capabilities: {
    xlsxExport: {
      // Fixed Stammdaten headers emitted by the export-pipeline
      // (libs/angular-ui/.../export-pipeline.ts) translated via
      // applications.stammdaten.<key>.label. Test 4.5 checks the
      // header row contains these in any order.
      expectedColumns: [
        'Bewerbungs-ID',
        'Bewerber-ID',
        'Vorname',
        'Nachname',
        'E-Mail',
        'Status',
      ],
    },
    mails: {
      // BMT mail-template names available on the tum-som staging
      // customer. Used by the mails spec to pick a template in the
      // compose dialog. Templates are seeded by the BMT customer
      // import; missing templates surface as a clear "no template
      // available" failure in the spec.
      templates: ['Eingangsbestätigung', 'Ablehnung', 'Annahme'],
    },
    tags: {
      // Two tag names that exist on tum-som by convention. Tests
      // create the application, assign one tag, swap to the other,
      // remove. Tag-settings are out of scope for the application
      // suite (see proposal Non-Goals).
      sampleNames: ['e2e-tag-A', 'e2e-tag-B'],
    },
    // BMT renders the Convex reviewer-comment textarea
    // (`<masterev-application-comments-convex>`) in the detail
    // sidebar. Framework spec at framework/specs/comments.spec.ts
    // asserts auto-save + reload persistence.
    comments: true,
    // BMT shows the attachments section with both local-storage
    // uploads and TUMonline (D3) read-only documents. Framework spec
    // at framework/specs/local-attachments.spec.ts covers the local
    // upload path only; D3 is out of scope.
    localAttachments: true,
    // BMT has a post-Stage-1 administrative step (`s2.export`,
    // TUMonline-Verbuchung) that must be completed even after a
    // Stage-1 rejection. The status-lifecycle spec uses this hint to
    // assert that `application.status` stays `in_progress` after
    // Stage 1 and only finalises once `s2.export` is done.
    // ADR-0018 Update 2026-05-11 / openspec change
    // `defer-application-status-finalization`.
    statusLifecycleDeferredFinalization: true,
    permissions: {
      // Short labels used by the framework's permissions spec to seed
      // teams with deterministic names. Picked short to keep the
      // generated team names well within Convex's index/team-name
      // length budget while remaining unique per role.
      writeTeamShort: 'twrite',
      readTeamShort: 'tread',
      seedApplicationWithStepPermission: async ({
        customer,
        teamId,
        access,
      }) => {
        const created = await apps.createApplication({
          customer,
          process: PROCESS,
          stammdaten: {
            givenName: 'E2E-Perm',
            familyName: 'Test',
            email: 'perm@e2e.test',
          },
          meta: { SV_ZEUGNIS_DURCHSCHNITT: '1,3' },
        });
        // First writable step of BMT is `s1.testresults`. Pass the
        // explicit (stage, step) so the helper does not have to guess
        // by creation order — this stays correct even if BMT later
        // adds parallel deps-free steps.
        const step = await convexRun<{ _id: string } | null>(
          'e2e:findFirstStep',
          {
            applicationId: created._id,
            stage: 's1',
            step: 'testresults',
          }
        );
        if (!step) {
          throw new Error(
            'BMT seed expected the first step to exist after createApplication; got null'
          );
        }
        await convexRun('e2e:setStepPermissions', {
          applicationStepId: step._id,
          permissions: [{ type: 'team', teamId, access }],
        });
        return created;
      },
    },
  },
};
