/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import { createServer } from 'node:http';
import { createProxyServer } from 'http-proxy';
import { AddressInfo } from 'node:net';
import puppeteer, { Browser, Page } from 'puppeteer';
import { executeDevServer } from '../../index';
import { describeServeBuilder } from '../jasmine-helpers';
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare const document: any;

interface ProxyInstance {
  server: typeof createProxyServer extends () => infer R ? R : never;
  url: string;
}

function findFreePort(): Promise<number> {
  return new Promise<number>((resolve, reject) => {
    const server = createServer();
    server.once('listening', () => {
      const port = (server.address() as AddressInfo).port;
      server.close((e) => (e ? reject(e) : resolve(port)));
    });
    server.once('error', (e) => server.close(() => reject(e)));
    server.listen();
  });
}

async function createProxy(target: string, secure: boolean, ws = true): Promise<ProxyInstance> {
  const proxyPort = await findFreePort();
  const server = createProxyServer({
    ws,
    target,
    secure,
    ssl: secure && {
      key: `
-----BEGIN RSA PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEBRUsUz4rdcMt
CQGLvG3SzUinsmgdgOyTNQNA0eOMyRSrmS8L+F/kSLUnqqu4mzdeqDzo2Xj553jK
dRqMCRFGJuGnQ/VIbW2A+ywgrqILuDyF5i4PL1aQW4yJ7TnXfONKfpswQArlN6DF
gBYJtoJlf8XD1sOeJpsv/O46/ix/wngQ+GwQQ2cfqxQT0fE9SBCY23VNt3SPUJ3k
9etJMvJ9U9GHSb1CFdNQe7Gyx7xdKf1TazB27ElNZEg2aF99if47uRskYjvvFivy
7nxGx/ccIwjwNMpk29AsKG++0sn1yTK7tD5Px6aCSVK0BKbdXZS2euJor8hASGBJ
3GpVGJvdAgMBAAECggEAapYo8TVCdPdP7ckb4hPP0/R0MVu9aW2VNmZ5ImH+zar5
ZmWhQ20HF2bBupP/VB5yeTIaDLNUKO9Iqy4KBWNY1UCHKyC023FFPgFV+V98FctU
faqwGOmwtEZToRwxe48ZOISndhEc247oCPyg/x8SwIY9z0OUkwaDFBEAqWtUXxM3
/SPpCT5ilLgxnRgVB8Fj5Z0q7ThnxNVOmVC1OSIakEj46PzmMXn1pCKLOCUmAAOQ
BnrOZuty2b8b2M/GHsktLZwojQQJmArnIBymTXQTVhaGgKSyOv1qvHLp9L1OJf0/
Xm+/TqT6ztzhzlftcObdfQZZ5JuoEwlvyrsGFlA3MQKBgQDiQC3KYMG8ViJkWrv6
XNAFEoAjVEKrtirGWJ66YfQ9KSJ7Zttrd1Y1V1OLtq3z4YMH39wdQ8rOD+yR8mWV
6Tnsxma6yJXAH8uan8iVbxjIZKF1hnvNCxUoxYmWOmTLcEQMzmxvTzAiR+s6R6Uj
9LgGqppt30nM4wnOhOJU6UxqbwKBgQDdy03KidbPZuycJSy1C9AIt0jlrxDsYm+U
fZrB6mHEZcgoZS5GbLKinQCdGcgERa05BXvJmNbfZtT5a37YEnbjsTImIhDiBP5P
nW36/9a3Vg1svd1KP2206/Bh3gfZbgTsQg4YogXgjf0Uzuvw18btgTtLVpVyeuqz
TU3eeF30cwKBgQCN6lvOmapsDEs+T3uhqx4AUH53qp63PmjOSUAnANJGmsq6ROZV
HmHAy6nn9Qpf85BRHCXhZWiMoIhvc3As/EINNtWxS6hC/q6jqp4SvcD50cVFBroY
/16iWGXZCX+37A+DSOfTWgSDPEFcKRx41UOpStHbITgVgEPieo/NWxlHmQKBgQDX
JOLs2RB6V0ilnpnjdPXzvncD9fHgmwvJap24BPeZX3HtXViqD76oZsu1mNCg9EW3
zk3pnEyyoDlvSIreZerVq4kN3HWsCVP3Pqr0kz9g0CRtmy8RWr28hjHDfXD3xPUZ
iGnMEz7IOHOKv722/liFAprV1cNaLUmFbDNg3jmlaQKBgQDG5WwngPhOHmjTnSml
amfEz9a4yEhQqpqgVNW5wwoXOf6DbjL2m/maJh01giThj7inMcbpkZlIclxD0Eu6
Lof+ctCeqSAJvaVPmd+nv8Yp26zsF1yM8ax9xXjrIvv9fSbycNveGTDCsNNTiYoW
QyvMqmN1kGy20SZbQDD/fLfqBQ==
-----END RSA PRIVATE KEY-----
    `,
      cert: `
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJALz8gD/gAt0OMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDIzMTgyMTQ5WhcNMTkxMDIzMTgyMTQ5WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAxAUVLFM+K3XDLQkBi7xt0s1Ip7JoHYDskzUDQNHjjMkUq5kvC/hf5Ei1
J6qruJs3Xqg86Nl4+ed4ynUajAkRRibhp0P1SG1tgPssIK6iC7g8heYuDy9WkFuM
ie0513zjSn6bMEAK5TegxYAWCbaCZX/Fw9bDniabL/zuOv4sf8J4EPhsEENnH6sU
E9HxPUgQmNt1Tbd0j1Cd5PXrSTLyfVPRh0m9QhXTUHuxsse8XSn9U2swduxJTWRI
NmhffYn+O7kbJGI77xYr8u58Rsf3HCMI8DTKZNvQLChvvtLJ9ckyu7Q+T8emgklS
tASm3V2UtnriaK/IQEhgSdxqVRib3QIDAQABo1AwTjAdBgNVHQ4EFgQUDZBhVKdb
3BRhLIhuuE522Vsul0IwHwYDVR0jBBgwFoAUDZBhVKdb3BRhLIhuuE522Vsul0Iw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABh9WWZwWLgb9/DcTxL72
6pI96t4jiF79Q+pPefkaIIi0mE6yodWrTAsBQu9I6bNRaEcCSoiXkP2bqskD/UGg
LwUFgSrDOAA3UjdHw3QU5g2NocduG7mcFwA40TB98sOsxsUyYlzSyWzoiQWwPYwb
hek1djuWkqPXsTjlj54PTPN/SjTFmo4p5Ip6nbRf2nOREl7v0rJpGbJvXiCMYyd+
Zv+j4mRjCGo8ysMR2HjCUGkYReLAgKyyz3M7i8vevJhKslyOmy6Txn4F0nPVumaU
DDIy4xXPW1STWfsmSYJfYW3wa0wk+pJQ3j2cTzkPQQ8gwpvM3U9DJl43uwb37v6I
7Q==
-----END CERTIFICATE-----
    `,
    },
  }).listen(proxyPort);

  return {
    server,
    url: `${secure ? 'https' : 'http'}://localhost:${proxyPort}`,
  };
}

async function goToPageAndWaitForWS(page: Page, url: string): Promise<void> {
  const baseUrl = url.replace(/^http/, 'ws');
  const socksRequest =
    baseUrl[baseUrl.length - 1] === '/' ? `${baseUrl}ng-cli-ws` : `${baseUrl}/ng-cli-ws`;
  // Create a Chrome dev tools session so that we can capturing websocket request.
  // https://github.com/puppeteer/puppeteer/issues/2974

  // We do this, to ensure that we make the right request with the expected host, port etc...
  const client = await page.target().createCDPSession();
  await client.send('Network.enable');
  await client.send('Page.enable');

  await Promise.all([
    new Promise<void>((resolve, reject) => {
      const timeout = setTimeout(
        () => reject(new Error(`A Websocket connected to ${socksRequest} was not established.`)),
        2000,
      );
      client.on('Network.webSocketCreated', ({ url }) => {
        if (url.startsWith(socksRequest)) {
          clearTimeout(timeout);
          resolve();
        }
      });
    }),
    page.goto(url),
  ]);

  await client.detach();
}

describeServeBuilder(
  executeDevServer,
  DEV_SERVER_BUILDER_INFO,
  (harness, setupTarget, isViteRun) => {
    // TODO(fix-vite): currently this is broken in vite.
    (isViteRun ? xdescribe : describe)(
      'Behavior: "Dev-server builder live-reload with proxies"',
      () => {
        let browser: Browser;
        let page: Page;

        const SERVE_OPTIONS = Object.freeze({
          ...BASE_OPTIONS,
          hmr: false,
          watch: true,
          liveReload: true,
        });

        beforeAll(async () => {
          browser = await puppeteer.launch({
            // MacOSX users need to set the local binary manually because Chrome has lib files with
            // spaces in them which Bazel does not support in runfiles
            // See: https://github.com/angular/angular-cli/pull/17624
            // eslint-disable-next-line max-len
            // executablePath: '/Users/<USERNAME>/git/angular-cli/node_modules/puppeteer/.local-chromium/mac-818858/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
            ignoreHTTPSErrors: true,
            args: ['--no-sandbox', '--disable-gpu'],
          });
        });

        afterAll(async () => {
          await browser.close();
        });

        beforeEach(async () => {
          setupTarget(harness, {
            polyfills: ['src/polyfills.ts'],
          });

          page = await browser.newPage();
        });

        afterEach(async () => {
          await page.close();
        });

        it('works without proxy', async () => {
          harness.useTarget('serve', {
            ...SERVE_OPTIONS,
          });

          await harness.writeFile('src/app/app.component.html', '<p>{{ title }}</p>');

          await harness.executeWithCases([
            async ({ result }) => {
              expect(result?.success).toBeTrue();
              if (typeof result?.baseUrl !== 'string') {
                throw new Error('Expected "baseUrl" to be a string.');
              }

              await goToPageAndWaitForWS(page, result.baseUrl);
              await harness.modifyFile('src/app/app.component.ts', (content) =>
                content.replace(`'app'`, `'app-live-reload'`),
              );
            },
            async ({ result }) => {
              expect(result?.success).toBeTrue();
              const innerText = await page.evaluate(() => document.querySelector('p').innerText);
              expect(innerText).toBe('app-live-reload');
            },
          ]);
        });

        it('works without http -> http proxy', async () => {
          harness.useTarget('serve', {
            ...SERVE_OPTIONS,
          });

          await harness.writeFile('src/app/app.component.html', '<p>{{ title }}</p>');

          let proxy: ProxyInstance | undefined;
          try {
            await harness.executeWithCases([
              async ({ result }) => {
                expect(result?.success).toBeTrue();
                if (typeof result?.baseUrl !== 'string') {
                  throw new Error('Expected "baseUrl" to be a string.');
                }

                proxy = await createProxy(result.baseUrl, false);
                await goToPageAndWaitForWS(page, proxy.url);
                await harness.modifyFile('src/app/app.component.ts', (content) =>
                  content.replace(`'app'`, `'app-live-reload'`),
                );
              },
              async ({ result }) => {
                expect(result?.success).toBeTrue();
                const innerText = await page.evaluate(() => document.querySelector('p').innerText);
                expect(innerText).toBe('app-live-reload');
              },
            ]);
          } finally {
            proxy?.server.close();
          }
        });

        it('works without https -> http proxy', async () => {
          harness.useTarget('serve', {
            ...SERVE_OPTIONS,
          });

          await harness.writeFile('src/app/app.component.html', '<p>{{ title }}</p>');

          let proxy: ProxyInstance | undefined;
          try {
            await harness.executeWithCases([
              async ({ result }) => {
                expect(result?.success).toBeTrue();
                if (typeof result?.baseUrl !== 'string') {
                  throw new Error('Expected "baseUrl" to be a string.');
                }

                proxy = await createProxy(result.baseUrl, true);
                await goToPageAndWaitForWS(page, proxy.url);
                await harness.modifyFile('src/app/app.component.ts', (content) =>
                  content.replace(`'app'`, `'app-live-reload'`),
                );
              },
              async ({ result }) => {
                expect(result?.success).toBeTrue();
                const innerText = await page.evaluate(() => document.querySelector('p').innerText);
                expect(innerText).toBe('app-live-reload');
              },
            ]);
          } finally {
            proxy?.server.close();
          }
        });
      },
    );
  },
);
