/**
 * Auth model (see apps/edr-freight-api + freight web apps):
 *  - POST {api}/api/auth/login  { email, password }
 *    → flattened body { success, token, refreshToken } (response interceptor
 *      flattens /api/auth responses — no .data nesting).
 *  - Both web apps read cookies `auth-token` / `refresh-token` and attach
 *    `Authorization: Bearer <token>`.
 *  - Cookies are port-agnostic on localhost, so portal and backoffice share
 *    one cookie jar. cy.session snapshots/restores cookies per session id,
 *    which keeps staff and customer sessions from clobbering each other —
 *    but inside a single test, switching apps requires re-invoking the
 *    matching login command first (see flows specs).
 */

export interface LoginBody {
  success: boolean;
  token: string;
  refreshToken: string;
}

const apiUrl = () => Cypress.env("apiUrl") as string;
const password = () => Cypress.env("defaultPassword") as string;

function apiLogin(email: string, pass?: string): Cypress.Chainable<LoginBody> {
  return cy
    .request<LoginBody>("POST", `${apiUrl()}/api/auth/login`, {
      email,
      password: pass ?? password(),
    })
    .then((response) => {
      expect(response.status).to.eq(201);
      expect(response.body.token, "login token").to.be.a("string");
      return cy.wrap(response.body, { log: false });
    });
}

function sessionFor(app: "backoffice" | "portal", email: string, pass?: string) {
  cy.session(
    [app, email],
    () => {
      apiLogin(email, pass).then(({ token, refreshToken }) => {
        cy.setCookie("auth-token", token);
        cy.setCookie("refresh-token", refreshToken);
      });
    },
    {
      cacheAcrossSpecs: true,
      validate() {
        cy.getCookie("auth-token").then((cookie) => {
          expect(cookie, "auth-token cookie").to.exist;
          cy.request({
            url: `${apiUrl()}/api/me`,
            headers: { Authorization: `Bearer ${cookie!.value}` },
          })
            .its("status")
            .should("eq", 200);
        });
      },
    },
  );
}

Cypress.Commands.add("apiLogin", (email: string, pass?: string) => apiLogin(email, pass));

Cypress.Commands.add("loginBackoffice", (email = "ceo@edr.local", pass?: string) => {
  sessionFor("backoffice", email, pass);
});

Cypress.Commands.add("loginPortal", (email = "user@gmail.com", pass?: string) => {
  // Demo portal users are seeded with a hardcoded password (DemoUsersSeeder),
  // unlike staff users which use DEFAULT_PASSWORD.
  sessionFor("portal", email, pass ?? (Cypress.env("demoPassword") as string));
});

Cypress.Commands.add("visitPortal", (path = "/") => {
  cy.visit(`${Cypress.env("portalUrl")}${path}`);
});

/**
 * Read the latest OTP the API generated for a contact. SMS/email delivery is
 * disabled in e2e (RABBITMQ_ENABLED=false) but the code is still stored in
 * freight.otp_verifications — keyed by normalized email (lowercased) or E.164
 * phone. Polls because the row is written async to the UI action.
 */
Cypress.Commands.add("getOtp", (target: string) => {
  const read = (attempt: number): Cypress.Chainable<string> =>
    cy
      .task<{ rows: Array<{ otp: string }> }>(
        "db:query",
        {
          sql: `SELECT otp FROM freight.otp_verifications
                WHERE email = $1 OR phone = $1
                ORDER BY updated_at DESC LIMIT 1`,
          params: [target],
        },
        { log: false },
      )
      .then((res) => {
        if (res.rows.length > 0) return cy.wrap(res.rows[0].otp, { log: false });
        expect(attempt, `OTP row for ${target}`).to.be.lessThan(20);
        return cy.wait(500, { log: false }).then(() => read(attempt + 1));
      });
  return read(0);
});

/** Open a Mantine <Select> by its label and pick an option by exact text. */
Cypress.Commands.add("mantineSelect", (label: string | RegExp, option: string | RegExp) => {
  cy.contains("label", label)
    .invoke("attr", "for")
    .then((id) => {
      cy.get(`[id="${id}"]`).click({ force: true });
    });
  // Scope to the OPEN listbox: closed dropdowns linger in the DOM, and two
  // selects on one page may list the same option text (e.g. the intercity
  // wizard's origin + destination both list every Ethiopian yard).
  //
  // The scope has to be the listbox rather than the options themselves. A long
  // list scrolls inside a max-height dropdown, and Cypress counts the clipped
  // rows as not-visible — matching on `[role="option"]:visible` silently drops
  // whatever sits past the fold (this hid the alphabetically-last trains).
  // force: the click still has to land on a row that needs scrolling to.
  cy.get('[role="listbox"]:visible')
    .last()
    .contains('[role="option"]', option)
    .click({ force: true });
});

/**
 * Fill the accept-contract modal's validity window. The modal used to offer a
 * dropdown of configured durations that defaulted to the first option; it now
 * takes explicit Start/End dates and pre-fills neither, so "Accept & start
 * approval" stays disabled until both are set.
 *
 * Mantine's DateInput parses typed text with its valueFormat, which defaults to
 * "MMMM D, YYYY" — the same shape en-US toLocaleDateString produces.
 */
Cypress.Commands.add("acceptValidityWindow", (days = 365) => {
  const start = new Date();
  const end = new Date(start.getTime() + days * 24 * 60 * 60 * 1000);
  const asInput = (d: Date) =>
    d.toLocaleDateString("en-US", {
      month: "long",
      day: "numeric",
      year: "numeric",
    });

  for (const [label, value] of [
    ["Start date", start],
    ["End date", end],
  ] as const) {
    cy.contains("label", label)
      .invoke("attr", "for")
      .then((id) => {
        cy.get(`[id="${id}"]`)
          .clear({ force: true })
          .type(asInput(value), { force: true })
          // DateInput commits on blur; it also closes the calendar popover,
          // which would otherwise sit over the submit button.
          .blur();
      });
  }
});

/**
 * Attach a company stamp in the open sign-contract modal. The stamp became a
 * REQUIRED field on signing — "Continue to verification" stays disabled without
 * one — and StampUpload only checks the MIME type and size before reading the
 * file as a data URL, so the smallest valid PNG is enough. The input is
 * `hidden` (a dropzone drives it), hence force.
 */
const STAMP_PNG =
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";

Cypress.Commands.add("uploadCompanyStamp", () => {
  cy.get('.mantine-Modal-content input[type="file"]')
    .first()
    .selectFile(
      {
        contents: Cypress.Buffer.from(STAMP_PNG, "base64"),
        fileName: "stamp.png",
        mimeType: "image/png",
      },
      { force: true },
    );
});

/**
 * Fill the per-booking "Cargo description" on the new-shipment form. It is
 * REQUIRED for container shipments (it moved from the contract to the booking),
 * and the form validates through react-hook-form's handleSubmit — so leaving it
 * blank aborts silently: no price modal, no request, no error toast.
 * No-op for bulk shipments, which have no such field.
 */
Cypress.Commands.add("fillCargoDescription", (text = "Electronics") => {
  cy.get("body").then(($b) => {
    const field = $b.find('[placeholder^="e.g. Electronics"]');
    if (!field.length) return;
    cy.wrap(field.first()).clear({ force: true }).type(text, { force: true });
  });
});

/**
 * Retire a previous run's contracts so a spec is re-runnable against a warm DB.
 *
 * The API allows one active contract per customer + service type + route, so
 * yesterday's contract 409s today's at creation — and staff accept contracts
 * with a year's validity, so it would keep doing so for a year.
 *
 * The cutoff is frozen on first use: Cypress re-evaluates the spec bundle on
 * cross-origin visits, so `before()` fires again mid-run, and a naive "cancel
 * this shape" would then cancel the contract THIS run had just created.
 * Anything created after the spec started is ours and must survive.
 */
Cypress.Commands.add(
  "retireStaleContracts",
  (opts: { tin: string; kind: string; direction: string }) => {
    // The once-per-run guard lives in the Node task (see cypress.config.ts).
    cy.task("db:retireStaleContracts", opts);
  },
);

/** Type a 6-digit code into a Mantine PinInput. */
Cypress.Commands.add("typeOtp", (code: string) => {
  cy.get(".mantine-PinInput-root input").should("have.length.at.least", code.length);
  code.split("").forEach((digit, i) => {
    cy.get(".mantine-PinInput-root input").eq(i).type(digit, { force: true });
  });
});

/**
 * Draw a squiggle on the signature-pad canvas (mouse events). When the account
 * already has a saved signature the modal opens in "Approve signature" mode
 * with no canvas — nothing to draw, the saved image is used as-is.
 */
Cypress.Commands.add("drawSignature", () => {
  cy.get(".mantine-Modal-content").then(($modals) => {
    if ($modals.find("canvas").length === 0) return;
    drawOnCanvas();
  });
});

function drawOnCanvas() {
  cy.get(".mantine-Modal-content canvas")
    .first()
    .then(($canvas) => {
      const rect = $canvas[0].getBoundingClientRect();
      const midX = rect.left + rect.width / 2;
      const midY = rect.top + rect.height / 2;
      cy.wrap($canvas)
        .trigger("mousedown", { clientX: midX - 60, clientY: midY, force: true })
        .trigger("mousemove", { clientX: midX - 20, clientY: midY - 15, force: true })
        .trigger("mousemove", { clientX: midX + 20, clientY: midY + 15, force: true })
        .trigger("mousemove", { clientX: midX + 60, clientY: midY, force: true })
        .trigger("mouseup", { force: true });
    });
}

declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace Cypress {
    interface Chainable {
      /** POST /api/auth/login, returns the flattened token body. */
      apiLogin(email: string, pass?: string): Chainable<LoginBody>;
      /** Cached programmatic staff session (default ceo@edr.local). */
      loginBackoffice(email?: string, pass?: string): Chainable<void>;
      /** Cached programmatic customer session (default user@gmail.com). */
      loginPortal(email?: string, pass?: string): Chainable<void>;
      /** cy.visit against the portal origin (env.portalUrl). */
      visitPortal(path?: string): Chainable<void>;
      /** Latest OTP stored for an email/phone (delivery is off in e2e). */
      getOtp(target: string): Chainable<string>;
      /** Open a Mantine Select by label, pick an option. */
      mantineSelect(label: string | RegExp, option: string | RegExp): Chainable<void>;
      /** Fill the accept-contract modal's Start/End validity dates. */
      acceptValidityWindow(days?: number): Chainable<void>;
      /** Attach the required company stamp in the sign-contract modal. */
      uploadCompanyStamp(): Chainable<void>;
      /** Fill the required per-booking cargo description (container only). */
      fillCargoDescription(text?: string): Chainable<void>;
      /** Cancel a previous run's contracts of this shape (warm-DB re-runs). */
      retireStaleContracts(opts: {
        tin: string;
        kind: string;
        direction: string;
      }): Chainable<void>;
      /** Fill a Mantine PinInput with a code. */
      typeOtp(code: string): Chainable<void>;
      /** Scribble on the signature-pad canvas inside the open modal. */
      drawSignature(): Chainable<void>;
    }
  }
}

export {};
