import {
  addDependenciesToPackageJson,
  addProjectConfiguration,
  readJson,
  Tree,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { nxVersion } from '../../utils/versions';

import { viteConfigurationGenerator } from './configuration';
import {
  mockAngularAppGenerator,
  mockReactAppGenerator,
  mockReactLibNonBuildableJestTestRunnerGenerator,
  mockReactLibNonBuildableVitestRunnerGenerator,
  mockReactMixedAppGenerator,
  mockUnknownAppGenerator,
  mockWebAppGenerator,
} from '../../utils/test-utils';

describe('@nx/vite:configuration', () => {
  let tree: Tree;

  describe('transform React app to use Vite', () => {
    beforeAll(async () => {
      tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
      mockReactAppGenerator(tree);
      const existing = 'existing';
      const existingVersion = '1.0.0';
      addDependenciesToPackageJson(
        tree,
        { '@nx/vite': nxVersion, [existing]: existingVersion },
        { [existing]: existingVersion }
      );
      await viteConfigurationGenerator(tree, {
        uiFramework: 'react',
        project: 'my-test-react-app',
      });
    });

    it('should add vite packages and react-related dependencies for vite', async () => {
      const packageJson = readJson(tree, '/package.json');
      expect(packageJson.devDependencies).toMatchObject({
        vite: expect.any(String),
        '@vitejs/plugin-react': expect.any(String),
      });
    });

    it('should move index.html to the root of the project', () => {
      expect(tree.exists('apps/my-test-react-app/src/index.html')).toBeFalsy();
      expect(tree.exists('apps/my-test-react-app/index.html')).toBeTruthy();
      expect(
        tree.read('apps/my-test-react-app/index.html', 'utf-8')
      ).toMatchSnapshot();
    });

    it('should create correct tsconfig compilerOptions', () => {
      const tsconfigJson = readJson(
        tree,
        'apps/my-test-react-app/tsconfig.json'
      );
      expect(tsconfigJson.compilerOptions.types).toMatchObject(['vite/client']);
    });

    it('should create vite.config file at the root of the app', () => {
      expect(tree.exists('apps/my-test-react-app/vite.config.ts')).toBe(true);
      expect(
        tree.read('apps/my-test-react-app/vite.config.ts', 'utf-8')
      ).toMatchSnapshot();
    });

    it('should transform workspace.json project config', () => {
      expect(tree.read('workspace.json', 'utf-8')).toMatchSnapshot();
    });
  });

  describe('transform Web app to use Vite', () => {
    beforeAll(async () => {
      tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
      mockWebAppGenerator(tree);
      const existing = 'existing';
      const existingVersion = '1.0.0';
      addDependenciesToPackageJson(
        tree,
        { '@nx/vite': nxVersion, [existing]: existingVersion },
        { [existing]: existingVersion }
      );
      await viteConfigurationGenerator(tree, {
        uiFramework: 'none',
        project: 'my-test-web-app',
      });
    });
    it('should add vite dependencies for vite', async () => {
      const packageJson = readJson(tree, '/package.json');
      expect(packageJson.devDependencies).toMatchObject({
        vite: expect.any(String),
      });
    });

    it('should move index.html to the root of the project', () => {
      expect(tree.exists('apps/my-test-web-app/src/index.html')).toBeFalsy();
      expect(tree.exists('apps/my-test-web-app/index.html')).toBeTruthy();
      expect(
        tree.read('apps/my-test-web-app/index.html', 'utf-8')
      ).toMatchSnapshot();
    });

    it('should create correct tsconfig compilerOptions', () => {
      const tsconfigJson = readJson(tree, 'apps/my-test-web-app/tsconfig.json');
      expect(tsconfigJson.compilerOptions.types).toMatchObject(['vite/client']);
    });

    it('should create vite.config file at the root of the app', () => {
      expect(tree.exists('apps/my-test-web-app/vite.config.ts')).toBe(true);
      expect(
        tree.read('apps/my-test-web-app/vite.config.ts', 'utf-8')
      ).toMatchSnapshot();
    });

    it('should transform workspace.json project config', () => {
      expect(tree.read('workspace.json', 'utf-8')).toMatchSnapshot();
    });
  });

  describe('do not transform Angular app to use Vite', () => {
    beforeAll(async () => {
      tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
      mockAngularAppGenerator(tree);
    });
    it('should throw when trying to convert', async () => {
      expect.assertions(2);

      try {
        await viteConfigurationGenerator(tree, {
          uiFramework: 'none',
          project: 'my-test-angular-app',
        });
      } catch (e) {
        expect(e).toBeDefined();
        expect(e.toString()).toContain(
          'The project my-test-angular-app cannot be converted to use the @nx/vite executors'
        );
      }
    });
  });

  describe('inform user of unknown targets when converting', () => {
    beforeAll(async () => {
      tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
      mockUnknownAppGenerator(tree);
    });

    it('should throw when trying to convert something unknown', async () => {
      const { Confirm } = require('enquirer');
      const confirmSpy = jest.spyOn(Confirm.prototype, 'run');
      confirmSpy.mockResolvedValue(true);
      expect.assertions(2);

      try {
        await viteConfigurationGenerator(tree, {
          uiFramework: 'none',
          project: 'my-test-random-app',
        });
      } catch (e) {
        expect(e).toBeDefined();
        expect(e.toString()).toContain(
          'Error: Cannot find apps/my-test-random-app/tsconfig.json'
        );
      }
    });

    it('should throw when trying to convert something unknown and user denies conversion', async () => {
      const { Confirm } = require('enquirer');
      const confirmSpy = jest.spyOn(Confirm.prototype, 'run');
      confirmSpy.mockResolvedValue(false);

      expect.assertions(2);

      try {
        await viteConfigurationGenerator(tree, {
          uiFramework: 'none',
          project: 'my-test-random-app',
        });
      } catch (e) {
        expect(e).toBeDefined();
        expect(e.toString()).toContain(
          'Nx could not verify that the executors you are using can be converted to the @nx/vite executors.'
        );
      }
    });
  });

  describe('transform React app to use Vite by providing custom targets', () => {
    describe('transform React app if supported executor is provided', () => {
      beforeEach(async () => {
        tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
        mockReactMixedAppGenerator(tree);
        const existing = 'existing';
        const existingVersion = '1.0.0';
        addDependenciesToPackageJson(
          tree,
          { '@nx/vite': nxVersion, [existing]: existingVersion },
          { [existing]: existingVersion }
        );
        await viteConfigurationGenerator(tree, {
          uiFramework: 'react',
          project: 'my-test-mixed-react-app',
          buildTarget: 'valid-build',
        });
      });
      it('should add vite packages and react-related dependencies for vite', async () => {
        const packageJson = readJson(tree, '/package.json');
        expect(packageJson.devDependencies).toMatchObject({
          vite: expect.any(String),
          '@vitejs/plugin-react': expect.any(String),
        });
      });

      it('should create vite.config file at the root of the app', () => {
        expect(tree.exists('apps/my-test-mixed-react-app/vite.config.ts')).toBe(
          true
        );
      });

      it('should transform workspace.json project config', () => {
        expect(tree.read('workspace.json', 'utf-8')).toMatchSnapshot();
      });
    });

    describe('do NOT transform React app if unsupported executor is provided', () => {
      beforeEach(async () => {
        tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
        mockReactMixedAppGenerator(tree);
        const existing = 'existing';
        const existingVersion = '1.0.0';
        addDependenciesToPackageJson(
          tree,
          { '@nx/vite': nxVersion, [existing]: existingVersion },
          { [existing]: existingVersion }
        );
      });
      it('should throw when trying to convert and user denies', async () => {
        const { Confirm } = require('enquirer');
        const confirmSpy = jest.spyOn(Confirm.prototype, 'run');
        confirmSpy.mockResolvedValue(false);
        expect.assertions(2);

        try {
          await viteConfigurationGenerator(tree, {
            uiFramework: 'none',
            project: 'my-test-mixed-react-app',
            buildTarget: 'invalid-build',
          });
        } catch (e) {
          expect(e).toBeDefined();
          expect(e.toString()).toContain(
            'The build target invalid-build cannot be converted to use the @nx/vite:build executor'
          );
        }
      });

      it('should NOT throw error when trying to convert and user confirms', async () => {
        const { Confirm } = require('enquirer');
        const confirmSpy = jest.spyOn(Confirm.prototype, 'run');
        confirmSpy.mockResolvedValue(true);
        expect.assertions(1);

        try {
          await viteConfigurationGenerator(tree, {
            uiFramework: 'none',
            project: 'my-test-mixed-react-app',
            buildTarget: 'invalid-build',
          });
          expect(
            tree.exists('apps/my-test-mixed-react-app/vite.config.ts')
          ).toBe(true);
        } catch (e) {
          throw new Error('Should not throw error');
        }
      });
    });
  });

  describe('vitest', () => {
    beforeAll(async () => {
      tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
      await mockReactAppGenerator(tree);
      const existing = 'existing';
      const existingVersion = '1.0.0';
      addDependenciesToPackageJson(
        tree,
        { '@nx/vite': nxVersion, [existing]: existingVersion },
        { [existing]: existingVersion }
      );
      await viteConfigurationGenerator(tree, {
        uiFramework: 'react',
        project: 'my-test-react-app',
        includeVitest: true,
      });
    });
    it('should create a vitest configuration if "includeVitest" is true', () => {
      const viteConfig = tree
        .read('apps/my-test-react-app/vite.config.ts')
        .toString();
      expect(viteConfig).toContain('test');
      expect(
        tree.read('apps/my-test-react-app/vite.config.ts', 'utf-8')
      ).toMatchSnapshot();
    });
  });

  describe('library mode', () => {
    beforeEach(async () => {
      tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
    });

    it('should add config for building library', async () => {
      addProjectConfiguration(tree, 'my-lib', {
        root: 'my-lib',
      });
      await viteConfigurationGenerator(tree, {
        uiFramework: 'react',
        includeLib: true,
        project: 'my-lib',
        newProject: true,
      });

      const viteConfig = tree.read('my-lib/vite.config.ts').toString();

      expect(viteConfig).toMatch('build: {');
      expect(viteConfig).toMatch("external: ['react'");
      expect(tree.read('my-lib/vite.config.ts', 'utf-8')).toMatchSnapshot();
    });

    it('should set up non buildable library correctly', async () => {
      mockReactLibNonBuildableJestTestRunnerGenerator(tree);
      await viteConfigurationGenerator(tree, {
        uiFramework: 'react',
        project: 'react-lib-nonb-jest',
        includeVitest: true,
      });
      expect(
        tree.read('libs/react-lib-nonb-jest/vite.config.ts', 'utf-8')
      ).toMatchSnapshot();

      expect(tree.read('workspace.json', 'utf-8')).toMatchSnapshot();
    });

    it('should set up non buildable library which already has vite.config.ts correctly', async () => {
      const { Confirm } = require('enquirer');
      const confirmSpy = jest.spyOn(Confirm.prototype, 'run');
      confirmSpy.mockResolvedValue(true);
      expect.assertions(2);

      mockReactLibNonBuildableVitestRunnerGenerator(tree);

      try {
        await viteConfigurationGenerator(tree, {
          uiFramework: 'react',
          project: 'react-lib-nonb-vitest',
          includeVitest: true,
        });
        expect(
          tree.read('libs/react-lib-nonb-vitest/vite.config.ts', 'utf-8')
        ).toMatchSnapshot();

        expect(tree.read('workspace.json', 'utf-8')).toMatchSnapshot();
      } catch (e) {
        throw new Error('Should not throw error');
      }
    });
  });
});
