import {parse} from 'postcss';
import {compileString} from 'sass';
import {runfiles} from '@bazel/runfiles';
import * as path from 'path';

import {createLocalAngularPackageImporter} from '../../../../../tools/sass/local-sass-importer';

// Note: For Windows compatibility, we need to resolve the directory paths through runfiles
// which are guaranteed to reside in the source tree.
const testDir = path.join(runfiles.resolvePackageRelative('../_all-theme.scss'), '../tests');
const packagesDir = path.join(runfiles.resolveWorkspaceRelative('src/cdk/_index.scss'), '../..');

const localPackageSassImporter = createLocalAngularPackageImporter(packagesDir);

/** Transpiles given Sass content into CSS. */
function transpile(content: string) {
  return compileString(
    `
        @use 'sass:list';
        @use 'sass:map';
        @use '../../../index' as mat;

        $internals: _mat-theming-internals-do-not-access;

        ${content}
      `,
    {
      loadPaths: [testDir],
      importers: [localPackageSassImporter],
    },
  ).css.toString();
}

function getRootVars(css: string) {
  const result: {[key: string]: string} = {};
  parse(css).each(node => {
    if (node.type === 'rule' && node.selector === ':root') {
      node.walk(child => {
        if (child.type === 'decl') {
          if (child.prop.startsWith('--')) {
            result[child.prop.substring(2)] = child.value;
          }
        }
      });
    }
  });
  return result;
}

describe('theming definition api', () => {
  describe('define-theme', () => {
    it('should fill in defaults', () => {
      const css = transpile(`
        $theme: mat.define-theme();
        $data: map.get($theme, $internals);
        :root {
          --keys: #{map.keys($data)};
          --version: #{map.get($data, theme-version)};
          --type: #{map.get($data, theme-type)};
          --palettes: #{map.keys(map.get($data, palettes))};
          --density: #{map.get($data, density-scale)};
          --base-tokens: #{list.length(map.get($data, base-tokens)) > 0};
          --color-tokens: #{list.length(map.get($data, color-tokens)) > 0};
          --typography-tokens: #{list.length(map.get($data, typography-tokens)) > 0};
          --density-tokens: #{list.length(map.get($data, density-tokens)) > 0};
          --color-system-variables-prefix: #{map.get($data, color-system-variables-prefix)};
          --typography-system-variables-prefix: #{map.get($data, typography-system-variables-prefix)};
        }
      `);
      const vars = getRootVars(css);
      expect(vars['keys'].split(', ')).toEqual([
        'theme-version',
        'theme-type',
        'palettes',
        'color-system-variables-prefix',
        'color-tokens',
        'font-definition',
        'typography-system-variables-prefix',
        'typography-tokens',
        'density-scale',
        'density-tokens',
        'base-tokens',
      ]);
      expect(vars['version']).toBe('1');
      expect(vars['type']).toBe('light');
      expect(vars['palettes'].split(', ')).toEqual([
        'primary',
        'secondary',
        'tertiary',
        'neutral',
        'neutral-variant',
        'error',
      ]);
      expect(vars['density']).toBe('0');
      expect(vars['base-tokens']).toBe('true');
      expect(vars['color-tokens']).toBe('true');
      expect(vars['typography-tokens']).toBe('true');
      expect(vars['density-tokens']).toBe('true');
      expect(vars['typography-system-variables-prefix']).toBe('mat-sys');
      expect(vars['color-system-variables-prefix']).toBe('mat-sys');
    });

    it('should customize colors', () => {
      const css = transpile(`
        $theme: mat.define-theme((
          color: (
            theme-type: dark,
            primary: mat.$yellow-palette,
            tertiary: mat.$red-palette,
          )
        ));
        $data: map.get($theme, $internals);
        :root {
          --token-surface: #{map.get($data, color-tokens, (mdc, theme), surface)};
          --token-primary: #{map.get($data, color-tokens, (mdc, theme), primary)};
          --token-secondary: #{map.get($data, color-tokens, (mdc, theme), secondary)};
          --token-tertiary: #{map.get($data, color-tokens, (mdc, theme), tertiary)};
          --palette-primary: #{map.get($data, palettes, primary, 50)};
          --palette-secondary: #{map.get($data, palettes, secondary, 50)};
          --palette-tertiary: #{map.get($data, palettes, tertiary, 50)};
          --type: #{map.get($data, theme-type)};
        }
      `);
      const vars = getRootVars(css);
      expect(vars['token-surface']).toBe('#14140f');
      expect(vars['token-primary']).toBe('#cdcd00');
      expect(vars['token-secondary']).toBe('#cac8a5');
      expect(vars['token-tertiary']).toBe('#ffb4a8');
      expect(vars['palette-primary']).toBe('#7b7b00');
      expect(vars['palette-secondary']).toBe('#7a795a');
      expect(vars['palette-tertiary']).toBe('#ef0000');
      expect(vars['type']).toBe('dark');
    });

    it('should customize typography', () => {
      const css = transpile(`
        $theme: mat.define-theme((
          typography: (
            brand-family: Comic Sans,
            plain-family: Wingdings,
            bold-weight: 300,
            medium-weight: 200,
            regular-weight: 100,
          )
        ));
        $data: map.get($theme, $internals);
        :root {
          --display-font:
            #{map.get($data, typography-tokens, (mdc, typography), display-large-font)};
          --display-weight:
            #{map.get($data, typography-tokens, (mdc, typography), display-large-weight)};
          --title-font:
            #{map.get($data, typography-tokens, (mdc, typography), title-small-font)};
          --title-weight:
            #{map.get($data, typography-tokens, (mdc, typography), title-small-weight)};
        }
      `);
      const vars = getRootVars(css);
      expect(vars['display-font']).toBe('Comic Sans');
      expect(vars['display-weight']).toBe('100');
      expect(vars['title-font']).toBe('Wingdings');
      expect(vars['title-weight']).toBe('200');
    });

    it('should customize density', () => {
      const css = transpile(`
        $theme: mat.define-theme((
          density: (
            scale: -2
          )
        ));
        $data: map.get($theme, $internals);
        :root {
          --size: #{map.get($data, density-tokens, (mdc, checkbox), state-layer-size)};
        }
      `);
      const vars = getRootVars(css);
      expect(vars['size']).toBe('32px');
    });

    it('should throw for invalid system config', () => {
      expect(() => transpile(`$theme: mat.define-theme(5)`)).toThrowError(
        /\$config should be a configuration object\. Got: 5/,
      );
    });

    it('should throw for invalid color config', () => {
      expect(() => transpile(`$theme: mat.define-theme((color: 5))`)).toThrowError(
        /\$config\.color should be a color configuration object\. Got: 5/,
      );
    });

    it('should throw for invalid typography config', () => {
      expect(() => transpile(`$theme: mat.define-theme((typography: 5))`)).toThrowError(
        /\$config\.typography should be a typography configuration object\. Got: 5/,
      );
    });

    it('should throw for invalid density config', () => {
      expect(() => transpile(`$theme: mat.define-theme((density: 5))`)).toThrowError(
        /\$config\.density should be a density configuration object\. Got: 5/,
      );
    });

    it('should throw for invalid config property', () => {
      expect(() => transpile(`$theme: mat.define-theme((fake: 5))`)).toThrowError(
        /\$config has unexpected properties.*Found: fake/,
      );
    });

    it('should throw for invalid color property', () => {
      expect(() => transpile(`$theme: mat.define-theme((color: (fake: 5)))`)).toThrowError(
        /\$config\.color has unexpected properties.*Found: fake/,
      );
    });

    it('should throw for invalid typography property', () => {
      expect(() => transpile(`$theme: mat.define-theme((typography: (fake: 5)))`)).toThrowError(
        /\$config\.typography has unexpected properties.*Found: fake/,
      );
    });

    it('should throw for invalid density property', () => {
      expect(() => transpile(`$theme: mat.define-theme((density: (fake: 5)))`)).toThrowError(
        /\$config\.density has unexpected properties.*Found: fake/,
      );
    });

    it('should throw for invalid theme type', () => {
      expect(() =>
        transpile(`$theme: mat.define-theme((color: (theme-type: black)))`),
      ).toThrowError(/Expected \$config\.color.theme-type to be one of:.*Got: black/);
    });

    it('should throw for invalid palette', () => {
      expect(() =>
        transpile(`$theme: mat.define-theme((color: (tertiary: mat.$m2-red-palette)))`),
      ).toThrowError(/Expected \$config\.color\.tertiary to be a valid M3 palette\. Got:/);
    });

    it('should throw for invalid density scale', () => {
      expect(() => transpile(`$theme: mat.define-theme((density: (scale: 10)))`)).toThrowError(
        /Expected \$config\.density\.scale to be one of:.*Got: 10/,
      );
    });
  });

  describe('define-colors', () => {
    it('should omit non-color info', () => {
      const css = transpile(`
        $theme: mat.define-colors();
        $data: map.get($theme, $internals);
        :root {
          --keys: #{map.keys($data)};
        }
      `);
      const vars = getRootVars(css);
      expect(vars['keys'].split(', ')).toEqual([
        'theme-version',
        'theme-type',
        'palettes',
        'color-system-variables-prefix',
        'color-tokens',
      ]);
    });
  });

  describe('define-typography', () => {
    it('should omit non-typography info', () => {
      const css = transpile(`
        $theme: mat.define-typography();
        $data: map.get($theme, $internals);
        :root {
          --keys: #{map.keys($data)};
        }
      `);
      const vars = getRootVars(css);
      expect(vars['keys'].split(', ')).toEqual([
        'theme-version',
        'font-definition',
        'typography-system-variables-prefix',
        'typography-tokens',
      ]);
    });
  });

  describe('define-density', () => {
    it('should omit non-density info', () => {
      const css = transpile(`
        $theme: mat.define-density();
        $data: map.get($theme, $internals);
        :root {
          --keys: #{map.keys($data)};
        }
      `);
      const vars = getRootVars(css);
      expect(vars['keys'].split(', ')).toEqual([
        'theme-version',
        'density-scale',
        'density-tokens',
      ]);
    });
  });
});
