import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import { serializeJson } from '../../src/utils/fileutils';

import * as path from 'path';
import { readJsonInTree, updateJsonInTree } from '../../src/utils/ast-utils';

describe('Update 7.2.0', () => {
  let initialTree: Tree;
  let schematicRunner: SchematicTestRunner;

  beforeEach(() => {
    initialTree = Tree.empty();
    createJson('package.json', {
      scripts: {}
    });
    createJson('tsconfig.json', {});
    createJson('angular.json', {
      projects: {
        app1: {
          root: 'apps/app1',
          architect: {
            build: {
              builder: '@angular-devkit/build-angular:browser',
              options: {
                tsConfig: 'apps/app1/tsconfig.app.json'
              }
            },
            test: {
              builder: '@angular-devkit/build-angular:karma',
              options: {
                tsConfig: 'apps/app1/tsconfig.spec.json'
              }
            },
            lint: {
              builder: '@angular-devkit/build-angular:tslint',
              options: {
                tsConfig: [
                  'apps/app1/tsconfig.app.json',
                  'apps/app1/tsconfig.spec.json'
                ]
              }
            }
          }
        },
        'app1-e2e': {
          root: 'apps/app1-e2e',
          architect: {
            e2e: {
              builder: '@angular-devkit/build-angular:protractor',
              options: {
                tsConfig: 'apps/app1-e2e/tsconfig.e2e.json'
              }
            },
            lint: {
              builder: '@angular-devkit/build-angular:tslint',
              options: {
                tsConfig: 'apps/app1-e2e/tsconfig.e2e.json'
              }
            }
          }
        },
        app2: {
          root: 'apps/app2',
          architect: {
            build: {
              builder: '@angular-devkit/build-angular:browser',
              options: {
                tsConfig: 'apps/app2/tsconfig.app.json'
              }
            },
            test: {
              builder: '@nrwl/schematics:jest',
              options: {
                tsConfig: 'apps/app2/tsconfig.spec.json'
              }
            },
            lint: {
              builder: '@angular-devkit/build-angular:tslint',
              options: {
                tsConfig: [
                  'apps/app2/tsconfig.app.json',
                  'apps/app2/tsconfig.spec.json'
                ]
              }
            }
          }
        },
        'app2-e2e': {
          root: 'apps/app2-e2e',
          architect: {
            e2e: {
              builder: '@nrwl/builders:cypress',
              options: {
                tsConfig: 'apps/app2-e2e/tsconfig.e2e.json'
              }
            },
            lint: {
              builder: '@angular-devkit/build-angular:tslint',
              options: {
                tsConfig: 'apps/app2-e2e/tsconfig.e2e.json'
              }
            }
          }
        },
        'node-app': {
          root: 'apps/node-app',
          architect: {
            build: {
              builder: '@nrwl/builders:node-build',
              options: {
                tsConfig: 'apps/node-app/tsconfig.app.json'
              }
            },
            test: {
              builder: '@nrwl/schematics:jest',
              options: {
                tsConfig: 'apps/node-app/tsconfig.spec.json'
              }
            },
            lint: {
              builder: '@angular-devkit/build-angular:tslint',
              options: {
                tsConfig: [
                  'apps/node-app/tsconfig.app.json',
                  'apps/node-app/tsconfig.spec.json'
                ]
              }
            }
          }
        },
        'weird-app': {
          root: 'apps/weird/app',
          architect: {
            build: {
              builder: '@nrwl/builders:node-build',
              options: {
                tsConfig: 'apps/weird/app/src/tsconfig.app.json'
              }
            },
            test: {
              builder: '@nrwl/schematics:jest',
              options: {
                tsConfig: 'apps/weird/app/src/tsconfig.spec.json'
              }
            },
            lint: {
              builder: '@angular-devkit/build-angular:tslint',
              options: {
                tsConfig: [
                  'apps/weird/app/src/tsconfig.app.json',
                  'apps/weird/app/src/tsconfig.spec.json'
                ]
              }
            }
          }
        },
        lib1: {
          root: 'libs/lib1',
          architect: {
            test: {
              builder: '@angular-devkit/build-angular:karma',
              options: {
                tsConfig: 'libs/lib1/tsconfig.spec.json'
              }
            },
            lint: {
              builder: '@angular-devkit/build-angular:tslint',
              options: {
                tsConfig: [
                  'libs/lib1/tsconfig.lib.json',
                  'libs/lib1/tsconfig.spec.json'
                ]
              }
            }
          }
        },
        lib2: {
          root: 'libs/lib2',
          architect: {
            test: {
              builder: '@angular-devkit/build-angular:jest',
              options: {
                tsConfig: 'libs/lib2/tsconfig.spec.json'
              }
            },
            lint: {
              builder: '@angular-devkit/build-angular:tslint',
              options: {
                tsConfig: [
                  'libs/lib2/tsconfig.lib.json',
                  'libs/lib2/tsconfig.spec.json'
                ]
              }
            }
          }
        }
      }
    });
    createJson('apps/app1/tsconfig.app.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: ['jquery']
      }
    });
    createJson('apps/app1/tsconfig.spec.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: ['jasmine', 'node', 'sinon']
      }
    });
    createJson('apps/app1-e2e/tsconfig.e2e.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: ['jasmine', 'jasminewd2', 'node']
      }
    });
    createJson('apps/app2/tsconfig.app.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: []
      }
    });
    createJson('apps/app2/tsconfig.spec.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: ['jest', 'node']
      }
    });
    createJson('apps/app2-e2e/tsconfig.e2e.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: ['cypress', 'node']
      }
    });
    createJson('apps/node-app/tsconfig.app.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: ['node']
      }
    });
    createJson('apps/node-app/tsconfig.spec.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: ['jest', 'node']
      }
    });
    createJson('apps/weird/app/src/tsconfig.app.json', {
      extends: '../../../tsconfig.json',
      compilerOptions: {}
    });
    createJson('apps/weird/app/src/tsconfig.spec.json', {
      extends: '../../../tsconfig.json',
      compilerOptions: {}
    });
    createJson('libs/lib1/tsconfig.lib.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: []
      }
    });
    createJson('libs/lib1/tsconfig.spec.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: ['jasmine', 'node']
      }
    });
    createJson('libs/lib2/tsconfig.lib.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: []
      }
    });
    createJson('libs/lib2/tsconfig.spec.json', {
      extends: '../../tsconfig.json',
      compilerOptions: {
        types: ['jest', 'node']
      }
    });

    function createJson(path: string, value: any) {
      initialTree.create(path, serializeJson(value));
    }

    schematicRunner = new SchematicTestRunner(
      '@nrwl/schematics',
      path.join(__dirname, '../migrations.json')
    );
  });

  it('should create tsconfigs for existing projects', async () => {
    const result = await schematicRunner
      .runSchematicAsync('update-7.2.0', {}, initialTree)
      .toPromise();
    expect(result.files).toContain('/tsconfig.json');
    expect(result.files).toContain('/apps/app1/tsconfig.json');
    expect(result.files).toContain('/apps/app1-e2e/tsconfig.json');
    expect(result.files).toContain('/apps/app2/tsconfig.json');
    expect(result.files).toContain('/apps/app2-e2e/tsconfig.json');
    expect(result.files).toContain('/apps/node-app/tsconfig.json');
    expect(result.files).toContain('/apps/weird/app/tsconfig.json');
    expect(result.files).toContain('/libs/lib1/tsconfig.json');
    expect(result.files).toContain('/libs/lib2/tsconfig.json');
    [
      '/apps/app1/tsconfig.json',
      '/apps/app1-e2e/tsconfig.json',
      '/apps/app2/tsconfig.json',
      '/apps/app2-e2e/tsconfig.json',
      '/apps/node-app/tsconfig.json',
      '/libs/lib1/tsconfig.json',
      '/libs/lib2/tsconfig.json'
    ].forEach(tsConfig => {
      const value = readJsonInTree(result, tsConfig);
      expect(value.extends).toEqual('../../tsconfig.json');
    });
    expect(
      readJsonInTree(result, 'apps/weird/app/tsconfig.json').extends
    ).toEqual('../../../tsconfig.json');
  });

  it('should fix cypress lint configs', async () => {
    initialTree = await schematicRunner
      .callRule(
        updateJsonInTree('angular.json', json => {
          json.projects['app2-e2e'].architect.lint.options.tsConfig =
            'e2e/tsconfig.e2e.json';
          return json;
        }),
        initialTree
      )
      .toPromise();
    const result = await schematicRunner
      .runSchematicAsync('update-7.2.0', {}, initialTree)
      .toPromise();
    expect(
      readJsonInTree(result, 'angular.json').projects['app2-e2e'].architect.lint
        .options.tsConfig
    ).toEqual('apps/app2-e2e/tsconfig.e2e.json');
    [
      '/apps/app1/tsconfig.app.json',
      '/apps/app1/tsconfig.spec.json',
      '/apps/app1-e2e/tsconfig.e2e.json',
      '/apps/app2/tsconfig.app.json',
      '/apps/app2/tsconfig.spec.json',
      '/apps/app2-e2e/tsconfig.e2e.json',
      '/apps/node-app/tsconfig.app.json',
      '/apps/node-app/tsconfig.spec.json',
      '/libs/lib1/tsconfig.lib.json',
      '/libs/lib1/tsconfig.spec.json',
      '/libs/lib2/tsconfig.lib.json',
      '/libs/lib2/tsconfig.spec.json'
    ].forEach(tsConfig => {
      const value = readJsonInTree(result, tsConfig);
      expect(value.extends).toEqual('./tsconfig.json');
    });
  });

  it('should not fail for non-existing tsconfigs', async () => {
    initialTree = await schematicRunner
      .callRule(
        updateJsonInTree('angular.json', json => {
          json.projects['app2'].architect.lint.options.tsConfig =
            'apps/nonexistent/tsconfig.app.json';
          return json;
        }),
        initialTree
      )
      .toPromise();
    await schematicRunner
      .runSchematicAsync('update-7.2.0', {}, initialTree)
      .toPromise();
  });

  it('should edit existing tsconfigs to extend the new one', async () => {
    const result = await schematicRunner
      .runSchematicAsync('update-7.2.0', {}, initialTree)
      .toPromise();
    [
      '/apps/app1/tsconfig.app.json',
      '/apps/app1/tsconfig.spec.json',
      '/apps/app1-e2e/tsconfig.e2e.json',
      '/apps/app2/tsconfig.app.json',
      '/apps/app2/tsconfig.spec.json',
      '/apps/app2-e2e/tsconfig.e2e.json',
      '/apps/node-app/tsconfig.app.json',
      '/apps/node-app/tsconfig.spec.json',
      '/libs/lib1/tsconfig.lib.json',
      '/libs/lib1/tsconfig.spec.json',
      '/libs/lib2/tsconfig.lib.json',
      '/libs/lib2/tsconfig.spec.json'
    ].forEach(tsConfig => {
      const value = readJsonInTree(result, tsConfig);
      expect(value.extends).toEqual('./tsconfig.json');
    });
    expect(
      readJsonInTree(result, 'apps/weird/app/src/tsconfig.app.json').extends
    ).toEqual('../tsconfig.json');
    expect(
      readJsonInTree(result, 'apps/weird/app/src/tsconfig.spec.json').extends
    ).toEqual('../tsconfig.json');
  });

  it('should edit existing tsconfigs to have a union of all types being used', async () => {
    const result = await schematicRunner
      .runSchematicAsync('update-7.2.0', {}, initialTree)
      .toPromise();

    function getTypes(path: string) {
      return readJsonInTree(result, path).compilerOptions.types;
    }

    expect(getTypes('apps/app1/tsconfig.json')).toEqual([
      'jquery',
      'jasmine',
      'node',
      'sinon'
    ]);
    expect(getTypes('apps/app1-e2e/tsconfig.json')).toEqual([
      'jasmine',
      'jasminewd2',
      'node'
    ]);
    expect(getTypes('apps/app2/tsconfig.json')).toEqual(['jest', 'node']);
    expect(getTypes('apps/app2-e2e/tsconfig.json')).toEqual([
      'cypress',
      'node'
    ]);
    expect(getTypes('apps/node-app/tsconfig.json')).toEqual(['node', 'jest']);
    expect(getTypes('apps/weird/app/tsconfig.json')).toBeUndefined();
    expect(getTypes('libs/lib1/tsconfig.json')).toEqual(['jasmine', 'node']);
    expect(getTypes('libs/lib2/tsconfig.json')).toEqual(['jest', 'node']);
  });

  it("should not set types if one of the project's tsconfigs do not have types defined", async () => {
    initialTree = await schematicRunner
      .callRule(
        updateJsonInTree('apps/app1/tsconfig.app.json', json => {
          delete json.compilerOptions.types;
          return json;
        }),
        initialTree
      )
      .toPromise();
    const result = await schematicRunner
      .runSchematicAsync('update-7.2.0', {}, initialTree)
      .toPromise();

    function getTypes(path: string) {
      return readJsonInTree(result, path).compilerOptions.types;
    }

    expect(getTypes('apps/app1/tsconfig.json')).toBeUndefined();
  });

  describe('tsconfig.json', () => {
    it('should be updated with es2015 modules', async () => {
      const result = await schematicRunner
        .runSchematicAsync('update-7.2.0', {}, initialTree)
        .toPromise();

      const tsConfig = readJsonInTree(result, 'tsconfig.json');
      expect(tsConfig.compilerOptions.module).toEqual('es2015');
    });
  });

  it('should update @ngrx dependencies to 6.1.2', async () => {
    const result = await schematicRunner
      .runSchematicAsync('update-7.2.0', {}, initialTree)
      .toPromise();

    const { dependencies, devDependencies } = JSON.parse(
      result.readContent('package.json')
    );

    expect(dependencies['@ngrx/effects']).toEqual('6.1.2');
    expect(dependencies['@ngrx/router-store']).toEqual('6.1.2');
    expect(dependencies['@ngrx/store']).toEqual('6.1.2');
    expect(devDependencies['@ngrx/schematics']).toEqual('6.1.2');
    expect(devDependencies['@ngrx/store-devtools']).toEqual('6.1.2');
  });
});
