import { storybookConfigurationGenerator } from '@nx/angular/generators';
import {
  NxJsonConfiguration,
  Tree,
  readNxJson,
  readProjectConfiguration,
  updateNxJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';
import { TsConfig } from '@nx/storybook/src/utils/utilities';

import { updateProjectConfiguration } from 'nx/src/generators/utils/project-configuration';

import { createTestApplication } from '../../utils/testing';
import { updateJson } from '../../utils/update-json';

import configureStorybook from './index';

describe('configure-storybook', () => {
  let warnSpy: jest.SpyInstance;

  function setupTest(): { tree: Tree } {
    warnSpy = jest.spyOn(console, 'warn');
    const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
    const nxJson: NxJsonConfiguration = readNxJson(tree) || {};
    nxJson.workspaceLayout = {
      appsDir: 'apps',
      libsDir: 'libs',
    };
    updateNxJson(tree, nxJson);

    tree.write('.gitignore', '');

    return { tree };
  }

  it('should configure storybook', async () => {
    const { tree } = setupTest();
    tree.write('.gitignore', '#');
    await createTestApplication(tree, {
      name: `test-app`,
      e2eTestRunner: true,
    });
    await storybookConfigurationGenerator(tree, {
      interactionTests: false,
      skipFormat: true,
      generateStories: false,
      linter: Linter.None,
      project: `test-app`,
    });
    await configureStorybook(tree, { name: 'test-app' });
    expect(tree.exists(`apps/test-app/.storybook/preview.js`)).toBeFalsy();
    expect(tree.exists(`apps/test-app/.storybook/preview.ts`)).toBeTruthy();
    expect(tree.read(`apps/test-app/.storybook/preview.ts`, 'utf-8')).toContain(
      `import { moduleMetadata } from '@storybook/angular';`,
    );
    const e2eConfig = readProjectConfiguration(tree, `test-app-e2e`);
    expect(e2eConfig.targets?.['e2e'].options.devServerTarget).toEqual(
      `test-app:storybook`,
    );
    let testAppConfig = readProjectConfiguration(tree, `test-app`);
    delete testAppConfig.targets?.['build'].options;
    updateProjectConfiguration(tree, `test-app`, testAppConfig);
    let testE2eAppConfig = readProjectConfiguration(tree, `test-app-e2e`);
    delete testE2eAppConfig.targets?.['e2e'].configurations;
    updateProjectConfiguration(tree, `test-app-e2e`, testE2eAppConfig);
    await configureStorybook(tree, { name: 'test-app' });
    expect(
      readProjectConfiguration(tree, `test-app`).targets?.['build'].options
        .styles.length,
    ).toBeGreaterThan(0);
    testAppConfig = readProjectConfiguration(tree, `test-app`);
    testAppConfig.targets = testAppConfig.targets || {};
    testAppConfig.targets['build'].options = {};
    updateProjectConfiguration(tree, `test-app`, testAppConfig);
    testE2eAppConfig = readProjectConfiguration(tree, `test-app-e2e`);
    delete testE2eAppConfig.targets?.['e2e'].options.baseUrl;
    updateProjectConfiguration(tree, `test-app-e2e`, testE2eAppConfig);
    await configureStorybook(tree, { name: 'test-app' });
    expect(
      readProjectConfiguration(tree, `test-app`).targets?.['build'].options
        .styles.length,
    ).toBeGreaterThan(0);
    expect(
      readProjectConfiguration(tree, `test-app-e2e`).targets?.['e2e'].options
        .devServerTarget,
    ).toEqual('test-app:storybook');
  });

  it('should configure storybook tsconfig', async () => {
    const { tree } = setupTest();
    tree.write('.gitignore', '#');
    await createTestApplication(tree, {
      name: `test-app`,
      e2eTestRunner: true,
    });
    await storybookConfigurationGenerator(tree, {
      interactionTests: false,
      skipFormat: true,
      generateStories: false,
      linter: Linter.None,
      project: `test-app`,
    });
    tree.delete(`apps/test-app/.storybook/tsconfig.json`);
    updateJson(
      tree,
      `apps/test-app/tsconfig.app.json`,
      (tsConfig: TsConfig) => {
        tsConfig.exclude = [];
        return tsConfig;
      },
    );
    await configureStorybook(tree, { name: 'test-app' });
    expect(tree.exists(`apps/test-app/.storybook/tsconfig.json`)).toBeTruthy();
    expect(
      JSON.parse(tree.read(`apps/test-app/tsconfig.app.json`, 'utf-8') || '{}')
        .exclude,
    ).toEqual(['jest.config.ts']);
  });

  it('should configure storybook tsconfig, add include and exclude', async () => {
    const { tree } = setupTest();
    tree.write('.gitignore', '#');
    await createTestApplication(tree, {
      name: `test-app`,
      e2eTestRunner: true,
    });
    await storybookConfigurationGenerator(tree, {
      interactionTests: false,
      skipFormat: true,
      generateStories: false,
      linter: Linter.None,
      project: `test-app`,
    });
    updateJson<TsConfig>(
      tree,
      `apps/test-app/.storybook/tsconfig.json`,
      (tsconfig) => {
        delete tsconfig.include;
        tsconfig.exclude =
          tsconfig.exclude?.filter((e) => e !== 'jest.config.ts') || [];
        return tsconfig;
      },
    );
    await configureStorybook(tree, { name: 'test-app' });
    expect(tree.exists(`apps/test-app/.storybook/tsconfig.json`)).toBeTruthy();
    expect(
      JSON.parse(
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        tree.read(`apps/test-app/.storybook/tsconfig.json`)!.toString(),
      ).include,
    ).toBeTruthy();
  });

  it('should error for missing e2e project', async () => {
    const { tree } = setupTest();
    tree.write('.gitignore', '#');
    await createTestApplication(tree, {
      name: `test-app`,
    });
    await storybookConfigurationGenerator(tree, {
      interactionTests: false,
      skipFormat: true,
      generateStories: false,
      linter: Linter.None,
      project: `test-app`,
    });
    await configureStorybook(tree, { name: 'test-app' });
    expect(warnSpy).toHaveBeenCalledWith(
      `Project "test-app-e2e" does not exist`,
    );
  });

  it('should error for e2e project without cypress', async () => {
    const { tree } = setupTest();
    tree.write('.gitignore', '#');
    await createTestApplication(tree, {
      name: `test-app`,
      e2eTestRunner: false,
    });
    await createTestApplication(tree, {
      name: `test-app-e2e`,
      e2eTestRunner: false,
    });
    await storybookConfigurationGenerator(tree, {
      interactionTests: false,
      skipFormat: true,
      generateStories: false,
      linter: Linter.None,
      project: `test-app`,
    });
    await configureStorybook(tree, { name: 'test-app' });
    expect(warnSpy).toHaveBeenCalledWith(
      `Project "test-app-e2e" does not have an e2e target with @nx/cypress:cypress`,
    );
  });
});
