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

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 '../../../index' as mat;

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

        $theme: mat.define-theme();

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

function union<T>(...sets: Set<T>[]): Set<T> {
  return new Set(sets.flatMap(s => [...s]));
}

function intersection<T>(set: Set<T>, ...sets: Set<T>[]): Set<T> {
  return new Set([...set].filter(i => sets.every(s => s.has(i))));
}

/** Expects the given warning to be reported in Sass. */
function expectWarning(message: RegExp) {
  expect(getMatchingWarning(message)).withContext('Expected warning to be printed.').toBeDefined();
}

/** Expects the given warning not to be reported in Sass. */
function expectNoWarning(message: RegExp) {
  expect(getMatchingWarning(message))
    .withContext('Expected no warning to be printed.')
    .toBeUndefined();
}

/**
 * Gets first instance of the given warning reported in Sass. Dart sass directly writes
 * to the `process.stderr` stream, so we spy on the `stderr.write` method. We
 * cannot expect a specific amount of writes as Sass calls `stderr.write` multiple
 * times for a warning (e.g. spacing and stack trace)
 */
function getMatchingWarning(message: RegExp) {
  const writeSpy = process.stderr.write as jasmine.Spy;
  return (writeSpy.calls?.all() ?? []).find(
    (s: jasmine.CallInfo<typeof process.stderr.write>) =>
      typeof s.args[0] === 'string' && message.test(s.args[0]),
  );
}

describe('M3 theme', () => {
  it('should emit all styles under the given selector', () => {
    const root = parse(
      transpile(`
      html {
        @include mat.all-component-themes($theme);
        @include mat.all-component-bases($theme);
        @include mat.all-component-colors($theme);
        @include mat.all-component-typographies($theme);
        @include mat.all-component-densities($theme);
      }
    `),
    );
    const selectors = new Set<string>();
    root.walkRules(rule => {
      selectors.add(rule.selector);
    });
    expect(Array.from(selectors)).toEqual(['html']);
  });

  it('should only emit CSS variables', () => {
    const root = parse(transpile(`html { @include mat.all-component-themes($theme); }`));
    const nonVarProps: string[] = [];
    root.walkDecls(decl => {
      if (!decl.prop.startsWith('--')) {
        nonVarProps.push(decl.prop);
      }
    });
    expect(nonVarProps).toEqual([]);
  });

  it('should not have overlapping tokens between theme dimensions', () => {
    const css = transpile(`
        $theme: mat.define-theme();
        base {
          @include mat.all-component-bases($theme);
        }
        color {
          @include mat.all-component-colors($theme);
        }
        typography {
          @include mat.all-component-typographies($theme);
        }
        density {
          @include mat.all-component-densities($theme);
        }
    `);
    const root = parse(css);
    const propSets: {[key: string]: Set<string>} = {};
    root.walkRules(rule => {
      rule.walkDecls(decl => {
        propSets[rule.selector] = propSets[rule.selector] || new Set();
        propSets[rule.selector].add(decl.prop);
      });
    });
    let overlap = new Set();
    for (const [dimension1, props1] of Object.entries(propSets)) {
      for (const [dimension2, props2] of Object.entries(propSets)) {
        if (dimension1 !== dimension2) {
          overlap = union(overlap, intersection(props1, props2));
        }
      }
    }
    expect([...overlap])
      .withContext('Did you forget to wrap these in `_hardcode()`?')
      .toEqual([]);
  });

  it('should throw if theme included at root', () => {
    expect(() => transpile(`@include mat.all-component-themes($theme)`)).toThrowError(
      /Calls to Angular Material theme mixins with an M3 theme must be wrapped in a selector/,
    );
  });

  describe('theme override API', () => {
    beforeEach(() => {
      spyOn(process.stderr, 'write').and.callThrough();
    });

    it('should allow overriding non-ambiguous token value', () => {
      const css = transpile(`
        div {
          @include mat.checkbox-overrides((selected-checkmark-color: magenta));
        }
      `);

      expect(css).toContain('--mdc-checkbox-selected-checkmark-color: magenta');
      expectNoWarning(/`selected-checkmark-color` is deprecated/);
    });

    it('should allow overriding ambiguous token value using prefix', () => {
      const css = transpile(`
        div {
          @include mat.form-field-overrides((filled-caret-color: magenta));
        }
      `);

      expect(css).toContain('--mdc-filled-text-field-caret-color: magenta');
      expect(css).not.toContain('--mdc-outline-text-field-caret-color: magenta');
      expectNoWarning(/`filled-caret-color` is deprecated/);
    });

    it('should allow overriding ambiguous token value without using prefix, but warn', () => {
      const css = transpile(`
        div {
          @include mat.form-field-overrides((caret-color: magenta));
        }
      `);

      expect(css).toContain('--mdc-filled-text-field-caret-color: magenta');
      expect(css).toContain('--mdc-outlined-text-field-caret-color: magenta');
      expectWarning(
        /Token `caret-color` is deprecated. Please use one of the following alternatives: filled-caret-color, outlined-caret-color/,
      );
    });

    it('should error on invalid token name', () => {
      expect(() =>
        transpile(`
        div {
          @include mat.form-field-overrides((fake: magenta));
        }
      `),
      ).toThrowError(/Invalid token name `fake`./);
    });

    it('should not error when calling theme override functions', () => {
      // Ensures that no components have issues with ambiguous token names.
      expect(() =>
        transpile(`
          html {
            @include mat.core-overrides(());
            @include mat.ripple-overrides(());
            @include mat.option-overrides(());
            @include mat.optgroup-overrides(());
            @include mat.pseudo-checkbox-overrides(());
            @include mat.autocomplete-overrides(());
            @include mat.badge-overrides(());
            @include mat.bottom-sheet-overrides(());
            @include mat.button-overrides(());
            @include mat.fab-overrides(());
            @include mat.icon-button-overrides(());
            @include mat.button-toggle-overrides(());
            @include mat.card-overrides(());
            @include mat.checkbox-overrides(());
            @include mat.chips-overrides(());
            @include mat.datepicker-overrides(());
            @include mat.dialog-overrides(());
            @include mat.divider-overrides(());
            @include mat.expansion-overrides(());
            @include mat.form-field-overrides(());
            @include mat.grid-list-overrides(());
            @include mat.icon-overrides(());
            @include mat.list-overrides(());
            @include mat.menu-overrides(());
            @include mat.paginator-overrides(());
            @include mat.progress-bar-overrides(());
            @include mat.progress-spinner-overrides(());
            @include mat.radio-overrides(());
            @include mat.select-overrides(());
            @include mat.sidenav-overrides(());
            @include mat.slide-toggle-overrides(());
            @include mat.slider-overrides(());
            @include mat.snack-bar-overrides(());
            @include mat.sort-overrides(());
            @include mat.stepper-overrides(());
            @include mat.table-overrides(());
            @include mat.tabs-overrides(());
            @include mat.toolbar-overrides(());
            @include mat.tooltip-overrides(());
            @include mat.tree-overrides(());
          }
        `),
      ).not.toThrow();
    });
  });
});
