import { assert } from '../../util/util.js';
import {
  TestParamsRW,
  JSONWithUndefined,
  badParamValueChars,
  paramKeyIsPublic,
} from '../params_utils.js';

import { parseParamValue } from './json_param_value.js';
import {
  TestQuery,
  TestQueryMultiFile,
  TestQueryMultiTest,
  TestQueryMultiCase,
  TestQuerySingleCase,
} from './query.js';
import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './separators.js';
import { validQueryPart } from './validQueryPart.js';

/**
 * converts foo/bar/src/webgpu/this/that/file.spec.ts to webgpu:this,that,file,*
 */
function convertPathToQuery(path: string) {
  // removes .spec.ts and splits by directory separators.
  const parts = path.substring(0, path.length - 8).split(/\/|\\/g);
  // Gets parts only after the last `src`. Example: returns ['webgpu', 'foo', 'bar', 'test']
  // for ['Users', 'me', 'src', 'cts', 'src', 'webgpu', 'foo', 'bar', 'test']
  const partsAfterSrc = parts.slice(parts.lastIndexOf('src') + 1);
  const suite = partsAfterSrc.shift();
  return `${suite}:${partsAfterSrc.join(',')},*`;
}

/**
 * If a query looks like a path (ends in .spec.ts and has directory separators)
 * then convert try to convert it to a query.
 */
function convertPathLikeToQuery(queryOrPath: string) {
  return queryOrPath.endsWith('.spec.ts') &&
    (queryOrPath.includes('/') || queryOrPath.includes('\\'))
    ? convertPathToQuery(queryOrPath)
    : queryOrPath;
}

/**
 * Convert long suite names (the part before the first colon) to the
 * shortest last word
 *    foo.bar.moo:test,subtest,foo -> moo:test,subtest,foo
 */
function shortenSuiteName(query: string) {
  const parts = query.split(':');
  // converts foo.bar.moo to moo
  const suite = parts.shift()?.replace(/.*\.(\w+)$/, '$1');
  return [suite, ...parts].join(':');
}

export function parseQuery(queryLike: string): TestQuery {
  try {
    const query = shortenSuiteName(convertPathLikeToQuery(queryLike));
    return parseQueryImpl(query);
  } catch (ex) {
    if (ex instanceof Error) {
      ex.message += `\n  on: ${queryLike}`;
    }
    throw ex;
  }
}

function parseQueryImpl(s: string): TestQuery {
  // Undo encodeURIComponentSelectively
  s = decodeURIComponent(s);

  // bigParts are: suite, file, test, params (note kBigSeparator could appear in params)
  let suite: string;
  let fileString: string | undefined;
  let testString: string | undefined;
  let paramsString: string | undefined;
  {
    const i1 = s.indexOf(kBigSeparator);
    assert(i1 !== -1, `query string must have at least one ${kBigSeparator}`);
    suite = s.substring(0, i1);
    const i2 = s.indexOf(kBigSeparator, i1 + 1);
    if (i2 === -1) {
      fileString = s.substring(i1 + 1);
    } else {
      fileString = s.substring(i1 + 1, i2);
      const i3 = s.indexOf(kBigSeparator, i2 + 1);
      if (i3 === -1) {
        testString = s.substring(i2 + 1);
      } else {
        testString = s.substring(i2 + 1, i3);
        paramsString = s.substring(i3 + 1);
      }
    }
  }

  const { parts: file, wildcard: filePathHasWildcard } = parseBigPart(fileString, kPathSeparator);

  if (testString === undefined) {
    // Query is file-level
    assert(
      filePathHasWildcard,
      `File-level query without wildcard ${kWildcard}. Did you want a file-level query \
(append ${kPathSeparator}${kWildcard}) or test-level query (append ${kBigSeparator}${kWildcard})?`
    );
    return new TestQueryMultiFile(suite, file);
  }
  assert(!filePathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`);

  const { parts: test, wildcard: testPathHasWildcard } = parseBigPart(testString, kPathSeparator);

  if (paramsString === undefined) {
    // Query is test-level
    assert(
      testPathHasWildcard,
      `Test-level query without wildcard ${kWildcard}; did you want a test-level query \
(append ${kPathSeparator}${kWildcard}) or case-level query (append ${kBigSeparator}${kWildcard})?`
    );
    assert(file.length > 0, 'File part of test-level query was empty (::)');
    return new TestQueryMultiTest(suite, file, test);
  }

  // Query is case-level
  assert(!testPathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`);

  const { parts: paramsParts, wildcard: paramsHasWildcard } = parseBigPart(
    paramsString,
    kParamSeparator
  );

  assert(test.length > 0, 'Test part of case-level query was empty (::)');

  const params: TestParamsRW = {};
  for (const paramPart of paramsParts) {
    const [k, v] = parseSingleParam(paramPart);
    assert(validQueryPart.test(k), `param key names must match ${validQueryPart}`);
    params[k] = v;
  }
  if (paramsHasWildcard) {
    return new TestQueryMultiCase(suite, file, test, params);
  } else {
    return new TestQuerySingleCase(suite, file, test, params);
  }
}

// webgpu:a,b,* or webgpu:a,b,c:*
const kExampleQueries = `\
webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}${kWildcard} or \
webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}c${kBigSeparator}${kWildcard}`;

function parseBigPart(
  s: string,
  separator: typeof kParamSeparator | typeof kPathSeparator
): { parts: string[]; wildcard: boolean } {
  if (s === '') {
    return { parts: [], wildcard: false };
  }
  const parts = s.split(separator);

  let endsWithWildcard = false;
  for (const [i, part] of parts.entries()) {
    if (i === parts.length - 1) {
      endsWithWildcard = part === kWildcard;
    }
    assert(
      part.indexOf(kWildcard) === -1 || endsWithWildcard,
      `Wildcard ${kWildcard} must be complete last part of a path (e.g. ${kExampleQueries})`
    );
  }
  if (endsWithWildcard) {
    // Remove the last element of the array (which is just the wildcard).
    parts.length = parts.length - 1;
  }
  return { parts, wildcard: endsWithWildcard };
}

function parseSingleParam(paramSubstring: string): [string, JSONWithUndefined] {
  assert(paramSubstring !== '', 'Param in a query must not be blank (is there a trailing comma?)');
  const i = paramSubstring.indexOf('=');
  assert(i !== -1, 'Param in a query must be of form key=value');
  const k = paramSubstring.substring(0, i);
  assert(paramKeyIsPublic(k), 'Param in a query must not be private (start with _)');
  const v = paramSubstring.substring(i + 1);
  return [k, parseSingleParamValue(v)];
}

function parseSingleParamValue(s: string): JSONWithUndefined {
  assert(
    !badParamValueChars.test(s),
    `param value must not match ${badParamValueChars} - was ${s}`
  );
  return parseParamValue(s);
}
