import type { Page, Route } from '@playwright/test';

// The mock mint lives on a .test origin that never resolves - every request
// to it is intercepted by Playwright and fulfilled in-process, so the specs
// never touch the network. Protocol shapes mirror lnurlcash-kit's client:
// the informational GET on the note URL (fetchNoteInfo) expects an LUD-03
// withdrawRequest echoing the queried k1, or the LNURL ERROR envelope; the
// rotation GET on the callback (rotateNote) expects {status: "OK"}. The same
// callback shape also answers a melt (meltNote sends k1 + pr to the same
// callback and only requires status "OK").
//
// Every method takes an optional origin, so a spec can stand up a SECOND
// mint (e.g. MINT2_ORIGIN) for inter-mint transfers.
export const MINT_ORIGIN = 'https://mint.test';
export const MINT2_ORIGIN = 'https://mint2.test';
export const NOTE_PATH = '/note';
export const CALLBACK_PATH = '/callback';

const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const fulfillJson = async (route: Route, body: unknown): Promise<void> => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    // the app runs on http://localhost:9333, so the mint is cross-origin -
    // keep Chromium's CORS check on the fulfilled response happy
    headers: { 'Access-Control-Allow-Origin': '*' },
    body: JSON.stringify(body),
  });
};

interface MockNoteInfoOptions {
  // note value in msat, reported as maxWithdrawable
  amountMsat?: number;
  // when set, the mint answers the ERROR envelope with this reason instead
  // (a reason matching /spent/i surfaces the "already been spent" UI)
  spentReason?: string;
  // the mint's signing pubkey, advertised as mintPubkey in responses
  mintPubkey?: string;
}

interface MockTargetMintOptions {
  // the mint's signing pubkey (66 hex chars), advertised on the mint-address
  // (/.well-known/lnurlw/) and note-info responses - NEVER the payRequest:
  // LUD-25 announces it on the withdraw side only, and a too-generous mock
  // once masked a real bug
  mintPubkey: string;
  // the invoice this mint hands out (and reports back as settled) - keep it
  // amount-less (decodeBolt11AmountMsat returns null) so the kit skips the
  // amount cross-check
  invoice: string;
  // the payment preimage a settled verify reveals - 64 hex chars (it IS the
  // claimed note's secret)
  preimage: string;
  // value of the note a claim mints, in msat
  noteAmountMsat: number;
  // optional payRequest metadata advertising a receive fee, e.g.
  // '[["text/plain","Mint fees: 2000,0"]]' (flat 2000 msat, 0 ppm)
  mintFeeMetadata?: string;
}

export class MintMocker {
  constructor(private page: Page) {}

  // The note's informational GET: a spec-shaped withdrawRequest, or the
  // ERROR envelope for a note the mint considers spent. The kit validates
  // that the service echoes back the queried k1, so read it off the request.
  async mockNoteInfo(options: MockNoteInfoOptions, origin = MINT_ORIGIN): Promise<void> {
    await this.page.route(
      new RegExp(`^${escapeRegExp(origin + NOTE_PATH)}\\?`),
      async (route: Route) => {
        if (options.spentReason !== undefined) {
          await fulfillJson(route, { status: 'ERROR', reason: options.spentReason });
          return;
        }
        const k1 = new URL(route.request().url()).searchParams.get('k1') ?? '';
        await fulfillJson(route, {
          tag: 'withdrawRequest',
          callback: `${origin}${CALLBACK_PATH}`,
          k1,
          minWithdrawable: options.amountMsat ?? 0,
          maxWithdrawable: options.amountMsat ?? 0,
          defaultDescription: 'mock mint note',
          ...(options.mintPubkey ? { mintPubkey: options.mintPubkey } : {}),
        });
      },
    );
  }

  // The mutating callback GET: rotate (k1 + h params) and melt (k1 + pr
  // params) both expect a plain {status: "OK"} confirmation. No sig is
  // returned - a plain LUD-03-style service that does not sign notes.
  async mockRotateOk(origin = MINT_ORIGIN): Promise<void> {
    await this.page.route(
      new RegExp(`^${escapeRegExp(origin + CALLBACK_PATH)}\\?`),
      async (route: Route) => {
        await fulfillJson(route, { status: 'OK' });
      },
    );
  }

  // The mint-address discovery endpoint (LUD-25): announces the signing key
  // and the node stats a real lnurl-mint advertises. `nodeCapacity` is msat
  // under its WIRE name (no suffix) - the kit has to map it onto
  // nodeCapacityMsat, which is exactly the 0.1.0 spread bug this exercises.
  // The payLink points back at the lnurlp route below, as prepareMint treats
  // it as the authoritative place to read the payRequest from.
  private async mockMintAddress(options: MockTargetMintOptions, origin: string): Promise<void> {
    await this.page.route(
      new RegExp(`^${escapeRegExp(`${origin}/.well-known/lnurlw/`)}`),
      async (route: Route) => {
        await fulfillJson(route, {
          tag: 'withdrawRequest',
          callback: `${origin}${CALLBACK_PATH}`,
          minWithdrawable: 1000,
          maxWithdrawable: 100_000_000_000,
          mintPubkey: options.mintPubkey,
          payLink: `${origin}/.well-known/lnurlp/mint`,
          nodeCapacity: 500_000_000,
          nodeNumChannels: 4,
          nodeNumPeers: 6,
        });
      },
    );
  }

  // Everything the target side of a transfer (or a Lightning receive)
  // needs: the mint-address discovery endpoint carrying the mint metadata,
  // the payRequest at the standard mint@ address, the invoice callback, an
  // immediately-settled verify endpoint revealing the preimage, and the
  // note info + rotate the claim then performs.
  async mockTargetMint(options: MockTargetMintOptions, origin = MINT2_ORIGIN): Promise<void> {
    await this.mockMintAddress(options, origin);
    await this.mockNoteInfo(
      { amountMsat: options.noteAmountMsat, mintPubkey: options.mintPubkey },
      origin,
    );
    await this.mockRotateOk(origin);
    await this.page.route(
      new RegExp(`^${escapeRegExp(`${origin}/.well-known/lnurlp/`)}`),
      async (route: Route) => {
        await fulfillJson(route, {
          tag: 'payRequest',
          callback: `${origin}/pay`,
          minSendable: 1000,
          maxSendable: 100_000_000_000,
          withdrawLink: `${origin}${NOTE_PATH}`,
          metadata: options.mintFeeMetadata ?? '[]',
        });
      },
    );
    await this.page.route(
      new RegExp(`^${escapeRegExp(`${origin}/pay`)}\\?`),
      async (route: Route) => {
        await fulfillJson(route, {
          pr: options.invoice,
          verify: `${origin}/verify`,
        });
      },
    );
    await this.page.route(
      new RegExp(`^${escapeRegExp(`${origin}/verify`)}`),
      async (route: Route) => {
        await fulfillJson(route, {
          settled: true,
          preimage: options.preimage,
          pr: options.invoice,
        });
      },
    );
  }
}
