import { APIRequestContext, request } from '@playwright/test';
import { E2E } from '../../playwright.config';
import { OFFICER } from './officer';

/**
 * The workflow endpoints, called as the officer.
 *
 * Used where a test's subject is what an approval *does* — the seafarer
 * number, the activated record, the two child applications — rather than which
 * buttons produce it. Driving six sections of wizard and a review screen to
 * reach a completion effect would make those tests about forms.
 *
 * Nothing here writes to the database directly. The completion effect runs
 * inside the API's approval transaction, so a test that faked the status would
 * assert against an approval that never happened.
 */

/** Routes served by the applicant-facing controller rather than the review one. */
const APPLICANT_STEPS = new Set(['submit', 'resubmit']);

/**
 * Resolves every open remark on an application, as the applicant.
 *
 * `resubmit` refuses while any remain (`unresolved_remarks`) — the applicant is
 * expected to tick off each correction as they make it, which the portal does
 * per section. A test that only wants the round-trip still has to do it.
 */
export async function resolveOpenRemarks(
  applicationId: string,
  remarkIds: string[],
  applicant: { email: string; password: string },
): Promise<void> {
  await runWorkflow(
    applicationId,
    remarkIds.map((remarkId) => ({
      path: `remarks/${remarkId}/resolve`,
      method: 'patch' as const,
    })),
    applicant,
  );
}

/**
 * An authenticated API context for one account.
 *
 * Paths built against it are relative on purpose. `E2E.apiUrl` carries the
 * `/api` prefix, and a leading slash resolves against the *origin* —
 * `/auth/login` against `http://host/api` requests `http://host/auth/login`,
 * which 404s. Every path in this file is therefore written without one.
 */
async function contextFor(
  who: string,
  credentials: { email: string; password: string },
): Promise<APIRequestContext> {
  const context = await request.newContext({ baseURL: `${E2E.apiUrl}/` });
  const response = await context.post('auth/login', {
    data: { email: credentials.email, password: credentials.password },
  });
  if (!response.ok()) {
    throw new Error(
      `${who} login failed (${response.status()}): ${await response.text()}`,
    );
  }
  const body = await response.json();
  const token = body?.token ?? body?.accessToken ?? body?.access_token;
  if (!token) {
    throw new Error(`No access token in login response: ${JSON.stringify(body)}`);
  }

  await context.dispose();
  return request.newContext({
    baseURL: `${E2E.apiUrl}/`,
    extraHTTPHeaders: { Authorization: `Bearer ${token}` },
  });
}

function officerContext(): Promise<APIRequestContext> {
  return contextFor('Officer', OFFICER);
}

export interface WorkflowStep {
  /** Route under the review controller, e.g. `claim`, `final-approve`. */
  path: string;
  data?: Record<string, unknown>;
  /** Set when a step is expected to be refused — the refusal is the assertion. */
  expectFailure?: boolean;
  /** POST unless stated; the applicant's remark-resolve route is a PATCH. */
  method?: 'post' | 'patch';
}

/**
 * Runs a sequence of workflow calls against one application.
 *
 * Returns each step's status code so a caller can assert on a refusal as
 * readily as on a success — "an officer may not evaluate a registration" is a
 * result worth checking, not an error to swallow.
 */
export async function runWorkflow(
  applicationId: string,
  steps: WorkflowStep[],
  /**
   * The owner, required only when a step is applicant-side. `submit` and
   * `resubmit` are guarded by ownership, not permission — the officer holds
   * every permission but is not the applicant, so running them on the officer's
   * token is refused with `not_application_owner`.
   */
  applicant?: { email: string; password: string },
): Promise<number[]> {
  const officer = await officerContext();
  const needsApplicant = steps.some(
    (step) => APPLICANT_STEPS.has(step.path) || step.path.startsWith('remarks/'),
  );
  if (needsApplicant && !applicant) {
    throw new Error(
      `Steps [${steps
        .filter((s) => APPLICANT_STEPS.has(s.path) || s.path.startsWith('remarks/'))
        .map((s) => s.path)
        .join(', ')}] act as the applicant — pass their credentials to runWorkflow.`,
    );
  }
  const owner = needsApplicant && applicant
    ? await contextFor('Applicant', applicant)
    : null;
  const codes: number[] = [];

  try {
    for (const step of steps) {
      // Applicant-side actions (`submit`, `resubmit`) live on the
      // applications controller; everything an officer does is on the review
      // controller. Routing by step keeps callers from having to know.
      const isApplicantStep =
        APPLICANT_STEPS.has(step.path) || step.path.startsWith('remarks/');
      const base = isApplicantStep
        ? 'license-applications'
        : 'license-application-review';
      const api = isApplicantStep && owner ? owner : officer;
      const url = `${base}/${applicationId}/${step.path}`;
      const response =
        step.method === 'patch'
          ? await api.patch(url, { data: step.data ?? {} })
          : await api.post(url, { data: step.data ?? {} });
      codes.push(response.status());

      if (!step.expectFailure && !response.ok()) {
        throw new Error(
          `Step "${step.path}" failed (${response.status()}): ${await response.text()}`,
        );
      }
    }
  } finally {
    await officer.dispose();
    await owner?.dispose();
  }

  return codes;
}

/**
 * The standalone seafarer-registration endpoints — its own controller pair,
 * not the licence ones above. `submit` is the applicant's; everything else
 * is the officer's.
 */
export async function runRegistrationWorkflow(
  registrationId: string,
  steps: WorkflowStep[],
  applicant?: { email: string; password: string },
): Promise<number[]> {
  const officer = await officerContext();
  const needsApplicant = steps.some((step) => step.path === 'submit');
  if (needsApplicant && !applicant) {
    throw new Error('`submit` acts as the applicant — pass their credentials.');
  }
  const owner = needsApplicant && applicant
    ? await contextFor('Applicant', applicant)
    : null;
  const codes: number[] = [];
  try {
    for (const step of steps) {
      const isApplicantStep = step.path === 'submit';
      const base = isApplicantStep
        ? 'seafarer-registrations'
        : 'seafarer-registration-review';
      const api = isApplicantStep && owner ? owner : officer;
      const response = await api.post(`${base}/${registrationId}/${step.path}`, {
        data: step.data ?? {},
      });
      codes.push(response.status());
      if (!step.expectFailure && !response.ok()) {
        throw new Error(
          `Step "${step.path}" failed (${response.status()}): ${await response.text()}`,
        );
      }
    }
  } finally {
    await officer.dispose();
    await owner?.dispose();
  }
  return codes;
}

/** Approve — the whole officer path for a registration; there is no claim. */
export async function approveRegistration(registrationId: string): Promise<void> {
  await runRegistrationWorkflow(registrationId, [
    { path: 'approve', data: { remark: 'E2E approval' } },
  ]);
}

/**
 * The Seaman Book / BTC endpoints. `payments/bypass` is the applicant's;
 * confirm-payment, schedule-issuance, issue and reject are the officer's.
 */
export async function runDocumentWorkflow(
  documentId: string,
  steps: WorkflowStep[],
  applicant?: { email: string; password: string },
): Promise<number[]> {
  const officer = await officerContext();
  const isApplicantStep = (path: string) => path.startsWith('payments/');
  const needsApplicant = steps.some((step) => isApplicantStep(step.path));
  if (needsApplicant && !applicant) {
    throw new Error('`payments/*` acts as the applicant — pass their credentials.');
  }
  const owner = needsApplicant && applicant
    ? await contextFor('Applicant', applicant)
    : null;
  const codes: number[] = [];
  try {
    for (const step of steps) {
      const applicantStep = isApplicantStep(step.path);
      const base = applicantStep ? 'seafarer-documents' : 'seafarer-document-review';
      const api = applicantStep && owner ? owner : officer;
      const response = await api.post(`${base}/${documentId}/${step.path}`, {
        data: step.data ?? {},
      });
      codes.push(response.status());
      if (!step.expectFailure && !response.ok()) {
        throw new Error(
          `Step "${step.path}" failed (${response.status()}): ${await response.text()}`,
        );
      }
    }
  } finally {
    await officer.dispose();
    await owner?.dispose();
  }
  return codes;
}
