/**
 * @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.io/license
 */

import { Architect } from '@angular-devkit/architect';
import { getSystemPath, join, normalize, virtualFs } from '@angular-devkit/core';
import express from 'express'; // eslint-disable-line import/no-extraneous-dependencies
import * as http from 'http';
import { AddressInfo } from 'net';
import { createArchitect, host } from '../../testing/test-utils';

describe('AppShell Builder', () => {
  const target = { project: 'app', target: 'app-shell' };
  let architect: Architect;

  beforeEach(async () => {
    await host.initialize().toPromise();
    architect = (await createArchitect(host.root())).architect;
  });
  afterEach(async () => host.restore().toPromise());

  const appShellRouteFiles = {
    'src/styles.css': `
      p { color: #000 }
    `,
    'src/app/app-shell/app-shell.component.html': `
      <p>
        app-shell works!
      </p>
    `,
    'src/app/app-shell/app-shell.component.ts': `
      import { Component, OnInit } from '@angular/core';

      @Component({
        selector: 'app-app-shell',
        templateUrl: './app-shell.component.html',
      })
      export class AppShellComponent implements OnInit {

        constructor() { }

        ngOnInit() {
        }

      }
    `,
    'src/app/app.module.ts': `
      import { BrowserModule } from '@angular/platform-browser';
      import { NgModule } from '@angular/core';

      import { AppRoutingModule } from './app-routing.module';
      import { AppComponent } from './app.component';
      import { environment } from '../environments/environment';
      import { RouterModule } from '@angular/router';

      @NgModule({
        declarations: [
          AppComponent
        ],
        imports: [
          BrowserModule.withServerTransition({ appId: 'serverApp' }),
          AppRoutingModule,
          RouterModule
        ],
        providers: [],
        bootstrap: [AppComponent]
      })
      export class AppModule { }
    `,
    'src/app/app.server.module.ts': `
      import { NgModule } from '@angular/core';
      import { ServerModule } from '@angular/platform-server';

      import { AppModule } from './app.module';
      import { AppComponent } from './app.component';
      import { Routes, RouterModule } from '@angular/router';
      import { AppShellComponent } from './app-shell/app-shell.component';

      const routes: Routes = [ { path: 'shell', component: AppShellComponent }];

      @NgModule({
        imports: [
          AppModule,
          ServerModule,
          RouterModule.forRoot(routes),
        ],
        bootstrap: [AppComponent],
        declarations: [AppShellComponent],
      })
      export class AppServerModule {}
    `,
    'src/main.ts': `
      import { enableProdMode } from '@angular/core';
      import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

      import { AppModule } from './app/app.module';
      import { environment } from './environments/environment';

      if (environment.production) {
        enableProdMode();
      }

      document.addEventListener('DOMContentLoaded', () => {
        platformBrowserDynamic().bootstrapModule(AppModule)
        .catch(err => console.log(err));
      });
    `,
    'src/app/app-routing.module.ts': `
      import { NgModule } from '@angular/core';
      import { Routes, RouterModule } from '@angular/router';

      const routes: Routes = [];

      @NgModule({
        imports: [RouterModule.forRoot(routes)],
        exports: [RouterModule]
      })
      export class AppRoutingModule { }
    `,
    'src/app/app.component.html': `
      <router-outlet></router-outlet>
    `,
  };

  it('works (basic)', async () => {
    host.replaceInFile(
      'src/app/app.module.ts',
      / {4}BrowserModule/,
      `
      BrowserModule.withServerTransition({ appId: 'some-app' })
    `,
    );

    const run = await architect.scheduleTarget(target);
    const output = await run.result;
    await run.stop();

    expect(output.success).toBe(true);

    const fileName = 'dist/index.html';
    const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
    expect(content).toMatch(/Welcome to app!/);
  });

  it('works with route', async () => {
    host.writeMultipleFiles(appShellRouteFiles);
    const overrides = { route: 'shell' };

    const run = await architect.scheduleTarget(target, overrides);
    const output = await run.result;
    await run.stop();

    expect(output.success).toBe(true);
    const fileName = 'dist/index.html';
    const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
    expect(content).toContain('app-shell works!');
  });

  it('works with route and service-worker', async () => {
    host.writeMultipleFiles(appShellRouteFiles);
    host.writeMultipleFiles({
      'src/ngsw-config.json': `
        {
          "index": "/index.html",
          "assetGroups": [{
            "name": "app",
            "installMode": "prefetch",
            "resources": {
              "files": [
                "/favicon.ico",
                "/index.html",
                "/*.css",
                "/*.js"
              ]
            }
          }, {
            "name": "assets",
            "installMode": "lazy",
            "updateMode": "prefetch",
            "resources": {
              "files": [
                "/assets/**"
              ]
            }
          }]
        }
      `,
      'src/app/app.module.ts': `
        import { BrowserModule } from '@angular/platform-browser';
        import { NgModule } from '@angular/core';

        import { AppRoutingModule } from './app-routing.module';
        import { AppComponent } from './app.component';
        import { ServiceWorkerModule } from '@angular/service-worker';
        import { environment } from '../environments/environment';
        import { RouterModule } from '@angular/router';

        @NgModule({
          declarations: [
            AppComponent
          ],
          imports: [
            BrowserModule.withServerTransition({ appId: 'serverApp' }),
            AppRoutingModule,
            ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production }),
            RouterModule
          ],
          providers: [],
          bootstrap: [AppComponent]
        })
        export class AppModule { }
      `,
      'e2e/app.e2e-spec.ts': `
        import { browser, by, element } from 'protractor';

        it('should have ngsw in normal state', () => {
          browser.get('/');
          // Wait for service worker to load.
          browser.sleep(2000);
          browser.waitForAngularEnabled(false);
          browser.get('/ngsw/state');
          // Should have updated, and be in normal state.
          expect(element(by.css('pre')).getText()).not.toContain('Last update check: never');
          expect(element(by.css('pre')).getText()).toContain('Driver state: NORMAL');
        });
      `,
    });
    // This should match the browser target prod config.
    host.replaceInFile(
      'angular.json',
      '"buildOptimizer": true',
      '"buildOptimizer": true, "serviceWorker": true',
    );

    // We're changing the workspace file so we need to recreate the Architect instance.
    architect = (await createArchitect(host.root())).architect;

    const overrides = { route: 'shell' };
    const run = await architect.scheduleTarget(
      { ...target, configuration: 'production' },
      overrides,
    );
    const output = await run.result;
    await run.stop();

    expect(output.success).toBe(true);

    // Make sure the index is pre-rendering the route.
    const fileName = 'dist/index.html';
    const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
    expect(content).toContain('app-shell works!');

    // Serve the app using a simple static server.
    const app = express();
    app.use('/', express.static(getSystemPath(join(host.root(), 'dist')) + '/'));
    const server = await new Promise<http.Server>((resolve) => {
      const innerServer = app.listen(0, 'localhost', () => resolve(innerServer));
    });
    try {
      const serverPort = (server.address() as AddressInfo).port;
      // Load app in protractor, then check service worker status.
      const protractorRun = await architect.scheduleTarget(
        { project: 'app-e2e', target: 'e2e' },
        { baseUrl: `http://localhost:${serverPort}/`, devServerTarget: '' },
      );

      const protractorOutput = await protractorRun.result;
      await protractorRun.stop();

      expect(protractorOutput.success).toBe(true);
    } finally {
      // Close the express server.
      await new Promise<void>((resolve) => server.close(() => resolve()));
    }
  });

  it('critical CSS is inlined', async () => {
    host.writeMultipleFiles(appShellRouteFiles);
    const overrides = {
      route: 'shell',
      browserTarget: 'app:build:production,inline-critical-css',
    };

    const run = await architect.scheduleTarget(target, overrides);
    const output = await run.result;
    await run.stop();

    expect(output.success).toBe(true);
    const fileName = 'dist/index.html';
    const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));

    expect(content).toContain('app-shell works!');
    expect(content).toContain('p{color:#000}');
    expect(content).toMatch(
      /<link rel="stylesheet" href="styles\.[a-z0-9]+\.css" media="print" onload="this\.media='all'">/,
    );
  });
});
