import { describe, expect, it } from 'vitest';
import Parser from 'tree-sitter';
import JavaScript from 'tree-sitter-javascript';
import TypeScript from 'tree-sitter-typescript';
import {
  DATA_ROUTE_TABLE_SOURCE,
  extractDataRouteTableRoutes,
  scanDataRouteTables,
} from '../../src/core/ingestion/route-extractors/data-route-table.js';
import { JAVASCRIPT_HTTP_PLUGIN } from '../../src/core/group/extractors/http-patterns/node.js';

const jsParser = new Parser();
jsParser.setLanguage(JavaScript);

const compact = (source: string) =>
  extractDataRouteTableRoutes(jsParser.parse(source), 'src/routes.js').map((route) => ({
    path: route.routePath,
    method: route.httpMethod,
    handler: route.handlerName,
    source: route.source,
  }));

const dispatch = (table: string) => `
  for (const route of ${table}) {
    if (route.path === request.path && route.method === request.method) route.handler();
  }
`;

describe('data route table extraction', () => {
  it('extracts literal entries and preserves method + URL identity', () => {
    expect(
      compact(`
        const apiRoutes = [
          { path: '/users', method: 'get', handler: listUsers },
          { path: '/users', method: 'POST', handler: users.create, auth: true },
        ];
        ${dispatch('apiRoutes')}
      `),
    ).toEqual([
      {
        path: '/users',
        method: 'GET',
        handler: 'listUsers',
        source: DATA_ROUTE_TABLE_SOURCE,
      },
      {
        path: '/users',
        method: 'POST',
        handler: 'users.create',
        source: DATA_ROUTE_TABLE_SOURCE,
      },
    ]);
  });

  it('supports quoted keys, static templates, and one-level member handlers', () => {
    expect(
      compact(`
        const ROUTE_TABLE = [{
          'path': \`/auth/me\`,
          'method': 'GET',
          'handler': auth.getCurrentUser,
        }];
        ${dispatch('ROUTE_TABLE')}
      `),
    ).toMatchObject([{ path: '/auth/me', method: 'GET', handler: 'auth.getCurrentUser' }]);
  });

  it('decodes JavaScript escapes in static route strings and quoted keys', () => {
    expect(
      compact(`
        const routes = [{
          'p\\u0061th': \`\\/users\`,
          method: 'G\\x45T',
          handler: escaped,
        }];
        ${dispatch('routes')}
      `),
    ).toMatchObject([{ path: '/users', method: 'GET', handler: 'escaped' }]);
  });

  it('rejects malformed or legacy-octal route string escapes', () => {
    expect(
      compact(`
        const routes = [{ path: '/\\8users', method: 'GET', handler: malformed }];
        ${dispatch('routes')}
      `),
    ).toEqual([]);
  });

  it('rejects executable values on extra route properties', () => {
    expect(
      compact(`
        const routes = [{
          path: '/users',
          method: 'GET',
          handler: listUsers,
          metadata: buildMetadata(),
        }];
        ${dispatch('routes')}
      `),
    ).toEqual([]);
  });

  it('keeps declarative metadata on route entries', () => {
    expect(
      compact(`
        const routes = [{
          path: '/users',
          method: 'GET',
          handler: listUsers,
          auth: true,
          metadata: { audience: 'staff', flags: ['audit'] },
        }];
        ${dispatch('routes')}
      `),
    ).toMatchObject([{ path: '/users', method: 'GET', handler: 'listUsers' }]);
  });

  it('allows line and block comments between static route properties', () => {
    expect(
      compact(`
        const routes = [{
          path: '/comments',
          // The verb remains a direct literal.
          method: 'GET',
          /* The handler remains a direct designator. */
          handler: commented,
        }];
        ${dispatch('routes')}
      `),
    ).toMatchObject([{ path: '/comments', method: 'GET', handler: 'commented' }]);
  });

  it('has JavaScript / TypeScript grammar parity', () => {
    const source = `
      type Route = { path: string; method: string; handler: Function };
      const routes: Route[] = [{ path: '/typed', method: 'GET', handler: typedHandler }];
      ${dispatch('routes')}
    `;
    const tsParser = new Parser();
    tsParser.setLanguage(TypeScript.typescript);
    expect(scanDataRouteTables(tsParser.parse(source))).toMatchObject([
      { path: '/typed', method: 'GET', handlerName: 'typedHandler' },
    ]);
  });

  it.each([
    ['dynamic path', `const routes = [{ path: runtimePath, method: 'GET', handler: h }]`, 'routes'],
    [
      'dynamic method',
      `const routes = [{ path: '/x', method: runtimeMethod, handler: h }]`,
      'routes',
    ],
    [
      'called handler',
      `const routes = [{ path: '/x', method: 'GET', handler: makeHandler() }]`,
      'routes',
    ],
    [
      'inline handler',
      `const routes = [{ path: '/x', method: 'GET', handler: () => {} }]`,
      'routes',
    ],
    [
      'spread entry',
      `const routes = [{ ...base, path: '/x', method: 'GET', handler: h }]`,
      'routes',
    ],
    ['computed key', `const routes = [{ ['path']: '/x', method: 'GET', handler: h }]`, 'routes'],
    ['unknown verb', `const routes = [{ path: '/x', method: 'CONNECT', handler: h }]`, 'routes'],
    [
      'multi-level member',
      `const routes = [{ path: '/x', method: 'GET', handler: services.auth.h }]`,
      'routes',
    ],
    [
      'non-route binding',
      `const requests = [{ path: '/x', method: 'GET', handler: h }]`,
      'requests',
    ],
  ])('suppresses %s', (_name, source, table) => {
    expect(compact(`${source}; ${dispatch(table)}`)).toEqual([]);
  });

  it('suppresses an unconsumed route-named descriptor table', () => {
    expect(
      compact(`const mockRoutes = [{ path: '/admin', method: 'DELETE', handler: onResponse }];`),
    ).toEqual([]);
  });

  it('does not use a dispatch loop hidden in a nested function', () => {
    expect(
      compact(`
        const routes = [{ path: '/admin', method: 'DELETE', handler: removeAdmin }];
        function dispatch(path, method) {
          for (const route of routes) {
            if (route.path === request.path && route.method === request.method) route.handler();
          }
        }
      `),
    ).toEqual([]);
  });

  it('suppresses outbound client tables that pass fields to fetch and then callbacks', () => {
    expect(
      compact(`
        const routeRequests = [
          { path: '/admin', method: 'DELETE', handler: onResponse },
        ];
        for (const route of routeRequests) {
          fetch(route.path, { method: route.method }).then(route.handler);
        }
      `),
    ).toEqual([]);
  });

  it('requires path and method comparisons in the same guard as direct dispatch', () => {
    expect(
      compact(`
        const routes = [{ path: '/admin', method: 'DELETE', handler: onResponse }];
        for (const route of routes) {
          if (route.path) console.log(route.method);
          route.handler();
        }
      `),
    ).toEqual([]);
  });

  it('does not treat negated equality as a positive dispatch guard', () => {
    expect(
      compact(`
        const routes = [{ path: '/admin', method: 'DELETE', handler: removeAdmin }];
        for (const route of routes) {
          if (!(route.path === request.path && route.method === request.method)) {
            route.handler();
          }
        }
      `),
    ).toEqual([]);
  });

  it('does not treat disjunctive comparisons as a complete dispatch guard', () => {
    expect(
      compact(`
        const routes = [{ path: '/admin', method: 'DELETE', handler: removeAdmin }];
        for (const route of routes) {
          if (route.path === request.path || route.method === request.method) route.handler();
        }
      `),
    ).toEqual([]);
  });

  it.each([
    ['direct', 'route.path', 'route.method'],
    ['parenthesized', '(route.path)', '(route.method)'],
    ['computed', "route['path']", "route['method']"],
    ['unary', '+route.path', '+route.method'],
    ['self-derived', 'normalize(route.path)', 'normalize(route.method)'],
  ])(
    'does not accept %s route-field self-comparisons as dispatch evidence',
    (_name, path, method) => {
      expect(
        compact(`
        const routes = [{ path: '/admin', method: 'DELETE', handler: removeAdmin }];
        for (const route of routes) {
          if (route.path === ${path} && route.method === ${method}) route.handler();
        }
      `),
      ).toEqual([]);
    },
  );

  it('suppresses duplicate required keys but preserves route candidates for resolution', () => {
    expect(
      compact(`
        const routes = [
          { path: '/bad', path: '/other', method: 'GET', handler: bad },
          { path: '/once', method: 'GET', handler: first },
          { path: '/once', method: 'GET', handler: second },
        ];
        ${dispatch('routes')}
      `),
    ).toMatchObject([
      { path: '/once', method: 'GET', handler: 'first' },
      { path: '/once', method: 'GET', handler: 'second' },
    ]);
  });

  it('rejects literal-only guards that select only one table entry', () => {
    expect(
      compact(`
        const routes = [
          { path: '/only', method: 'GET', handler: only },
          { path: '/never', method: 'POST', handler: never },
        ];
        for (const route of routes) {
          if (route.path === '/only' && route.method === 'GET') route.handler();
        }
      `),
    ).toEqual([]);
  });

  it('rejects route-derived aliases even when their names resemble request fields', () => {
    expect(
      compact(`
        const routes = [{ path: '/copied', method: 'GET', handler: copied }];
        for (const route of routes) {
          const copiedPath = route.path;
          const copiedMethod = route.method;
          if (route.path === copiedPath && route.method === copiedMethod) route.handler();
        }
      `),
    ).toEqual([]);
  });

  it('requires request fields to come from an unshadowed ingress binding', () => {
    expect(
      compact(`
        const request = { path: '/only', method: 'GET' };
        const routes = [
          { path: '/only', method: 'GET', handler: only },
          { path: '/never', method: 'POST', handler: never },
        ];
        ${dispatch('routes')}
      `),
    ).toEqual([]);

    expect(
      compact(`
        const routes = [{ path: '/nested', method: 'GET', handler: nested }];
        for (const route of routes) {
          if (
            route.path === request.config.path &&
            route.method === request.options.method
          ) route.handler();
        }
      `),
    ).toEqual([]);

    expect(
      compact(`
        function dispatchRequest(request) {
          const routes = [{ path: '/parameter', method: 'GET', handler: parameterHandler }];
          for (const route of routes) {
            if (route.path === request.path && route.method === request.method) route.handler();
          }
        }
      `),
    ).toMatchObject([{ path: '/parameter', method: 'GET', handler: 'parameterHandler' }]);
  });

  it.each([
    ['reassigned request parameter', 'request = { path: "/x", method: "GET" };'],
    ['mutated request fields', 'request.path = "/x"; request.method = "GET";'],
    ['escaped request parameter', 'observe(request);'],
    [
      'request mutation through a called closure',
      'const overwrite = () => { request.path = "/x"; request.method = "GET"; }; overwrite();',
    ],
  ])('rejects an unstable ingress binding: %s', (_name, mutation) => {
    expect(
      compact(`
        function dispatchRequest(request) {
          const routes = [{ path: '/x', method: 'GET', handler: handler }];
          ${mutation}
          for (const route of routes) {
            if (route.path === request.path && route.method === request.method) route.handler();
          }
        }
      `),
    ).toEqual([]);
  });

  it.each([
    [
      'a route-literal narrowing conjunct',
      `route.path === request.path && route.method === request.method && route.path === '/only'`,
    ],
    [
      'a statically false conjunct',
      `route.path === request.path && route.method === request.method && false`,
    ],
    [
      'a statically false equality',
      `route.path === request.path && route.method === request.method && 1 === 2`,
    ],
  ])('rejects %s', (_name, condition) => {
    expect(
      compact(`
        const routes = [
          { path: '/only', method: 'GET', handler: only },
          { path: '/other', method: 'GET', handler: other },
        ];
        for (const route of routes) {
          if (${condition}) route.handler();
        }
      `),
    ).toEqual([]);
  });

  it('rejects a conditionally unreachable handler call in the consequence', () => {
    expect(
      compact(`
        const routes = [{ path: '/x', method: 'GET', handler: unreachable }];
        for (const route of routes) {
          if (route.path === request.path && route.method === request.method) {
            false && route.handler();
          }
        }
      `),
    ).toEqual([]);
  });

  it('rejects a handler call after a statically terminating branch', () => {
    expect(
      compact(`
        const routes = [{ path: '/x', method: 'GET', handler: unreachable }];
        for (const route of routes) {
          if (route.path === request.path && route.method === request.method) {
            if (true) return;
            route.handler();
          }
        }
      `),
    ).toEqual([]);
  });

  it.each(['return', 'throw new Error("stop")', 'break', 'continue'])(
    'rejects a handler call after an unconditional %s',
    (transfer) => {
      expect(
        compact(`
          const routes = [{ path: '/x', method: 'GET', handler: unreachable }];
          for (const route of routes) {
            if (route.path === request.path && route.method === request.method) {
              ${transfer};
              route.handler();
            }
          }
        `),
      ).toEqual([]);
    },
  );

  it.each(['return', 'throw new Error("stop")', 'break', 'continue'])(
    'rejects a dispatch guard after an unconditional %s',
    (transfer) => {
      expect(
        compact(`
          const routes = [{ path: '/x', method: 'GET', handler: unreachable }];
          for (const route of routes) {
            ${transfer};
            if (route.path === request.path && route.method === request.method) {
              route.handler();
            }
          }
        `),
      ).toEqual([]);
    },
  );

  it('rejects a dispatch guard nested in a statically false branch', () => {
    expect(
      compact(`
        const routes = [{ path: '/x', method: 'GET', handler: unreachable }];
        for (const route of routes) {
          if (false) {
            if (route.path === request.path && route.method === request.method) {
              route.handler();
            }
          }
        }
      `),
    ).toEqual([]);
  });

  it('rejects mutation of the loop entry before dispatch', () => {
    expect(
      compact(`
        const routes = [{ path: '/mutated', method: 'GET', handler: mutated }];
        for (const route of routes) {
          route.path = request.path;
          route.method = request.method;
          if (route.path === request.path && route.method === request.method) route.handler();
        }
      `),
    ).toEqual([]);

    expect(
      compact(`
        const routes = [{ path: '/reassigned', method: 'GET', handler: original }];
        for (let route of routes) {
          route = replacement;
          if (route.path === request.path && route.method === request.method) route.handler();
        }
      `),
    ).toEqual([]);
  });

  it.each([
    ['deleting an entry field', 'delete route.path;'],
    ['deleting a wrapped entry field', 'delete (route.path);'],
    ['writing through a destructuring target', '({ path: route.path } = request);'],
    ['augmenting the handler field', 'route.handler ||= replacement;'],
    ['aliasing the entry', 'const alias = route; alias.path = request.path;'],
  ])('rejects %s', (_name, mutation) => {
    expect(
      compact(`
        const routes = [{ path: '/mutated', method: 'GET', handler: original }];
        for (const route of routes) {
          ${mutation}
          if (route.path === request.path && route.method === request.method) route.handler();
        }
      `),
    ).toEqual([]);
  });

  it.each([
    [
      'a mutable declaration',
      `let routes = [{ path: '/stale', method: 'GET', handler: stale }];
       ${dispatch('routes')}`,
    ],
    [
      'a reassigned table',
      `const routes = [{ path: '/stale', method: 'GET', handler: stale }];
       routes = [];
       ${dispatch('routes')}`,
    ],
    [
      'an aliased table',
      `const routes = [{ path: '/stale', method: 'GET', handler: stale }];
       const alias = routes;
       ${dispatch('routes')}`,
    ],
    [
      'a truncated table',
      `const routes = [{ path: '/stale', method: 'GET', handler: stale }];
       routes.length = 0;
       ${dispatch('routes')}`,
    ],
  ])('rejects %s', (_name, source) => {
    expect(compact(source)).toEqual([]);
  });

  it.each([
    [
      'a reassigned bare handler',
      `function original() {}
       const routes = [{ path: '/stale', method: 'GET', handler: original }];
       original = replacement;
       ${dispatch('routes')}`,
    ],
    [
      'a reassigned member handler',
      `const auth = {};
       auth.handle = replacement;
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
    [
      'a handler owner mutated through an alias',
      `const auth = {};
       const alias = auth;
       alias.handle = replacement;
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
    [
      'a handler owner hidden by parentheses',
      `const auth = {};
       const alias = (auth);
       alias.handle = replacement;
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
    [
      'a handler owner nested in an initializer',
      `const auth = {};
       const box = { auth };
       box.auth.handle = replacement;
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
    [
      'a handler owner stored under an unrelated handler property',
      `const auth = {};
       const box = { handler: auth };
       box.handler.handle = replacement;
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
    [
      'a handler owner assigned to an alias',
      `const auth = {};
       let alias;
       alias = auth;
       alias.handle = replacement;
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
    [
      'an escaped handler owner',
      `const auth = {};
       auth.configure();
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
    [
      'a handler owner escaped through a computed member call',
      `const auth = {};
       auth['configure']();
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
    [
      'a parenthesized handler owner escaped through a member call',
      `const auth = {};
       (auth).configure();
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
    [
      'a returned handler owner',
      `const auth = {};
       function expose() { return auth; }
       expose().handle = replacement;
       const routes = [{ path: '/stale', method: 'GET', handler: auth.handle }];
       ${dispatch('routes')}`,
    ],
  ])('rejects %s', (_name, source) => {
    expect(compact(source)).toEqual([]);
  });

  it('preserves an unresolved identity tombstone for unsupported duplicate handlers', () => {
    expect(
      compact(`
        function valid() {}
        const routes = [
          { path: '/users', method: 'GET', handler: makeHandler() },
          { path: '/users', method: 'GET', handler: valid },
        ];
        ${dispatch('routes')}
      `),
    ).toEqual([]);
  });

  it('tombstones recoverable identities from entries with duplicate route keys', () => {
    expect(
      compact(`
        function first() {}
        function second() {}
        const routes = [
          { path: '/users', path: '/users', method: 'GET', handler: first },
          { path: '/users', method: 'GET', handler: second },
        ];
        ${dispatch('routes')}
      `),
    ).toEqual([]);
  });

  it('uses the final duplicate route keys when tombstoning an invalid entry', () => {
    expect(
      compact(`
        function first() {}
        function second() {}
        const routes = [
          { path: '/ignored', path: '/users', method: 'POST', method: 'GET', handler: first },
          { path: '/users', method: 'GET', handler: second },
        ];
        ${dispatch('routes')}
      `),
    ).toEqual([]);
  });

  it('suppresses later candidates when a trailing spread leaves identity unknown', () => {
    expect(
      compact(`
        function first() {}
        function second() {}
        const routes = [
          { path: '/unknown', method: 'POST', handler: first, ...dynamicRoute },
          { path: '/users', method: 'GET', handler: second },
        ];
        ${dispatch('routes')}
      `),
    ).toEqual([]);
  });

  it.each([
    ['a dynamic path', `{ path: runtimePath, method: 'GET', handler: first }`],
    [
      'a computed overwrite',
      `{ path: '/other', method: 'GET', handler: first, ['path']: '/users' }`,
    ],
  ])('suppresses later candidates after %s', (_name, firstEntry) => {
    expect(
      compact(`
        function first() {}
        function second() {}
        const routes = [
          ${firstEntry},
          { path: '/users', method: 'GET', handler: second },
        ];
        ${dispatch('routes')}
      `),
    ).toEqual([]);
  });

  it('rejects tables containing array-level spreads', () => {
    expect(
      compact(`
        function second() {}
        const routes = [
          ...baseRoutes,
          { path: '/users', method: 'GET', handler: second },
        ];
        ${dispatch('routes')}
      `),
    ).toEqual([]);
  });

  it('propagates unresolved identity tombstones across route tables', () => {
    expect(
      compact(`
        function first() {}
        function second() {}
        const primaryRoutes = [{ path: '/users', method: 'GET', handler: first }];
        const fallbackRoutes = [{ path: '/users', method: 'GET', handler: makeHandler() }];
        ${dispatch('primaryRoutes')}
        ${dispatch('fallbackRoutes')}
      `),
    ).toEqual([]);
  });

  it.each([
    [
      'an unstable duplicate handler binding',
      `function first() {}
       function second() {}
       first = replacement;
       const routes = [
         { path: '/users', method: 'GET', handler: first },
         { path: '/users', method: 'GET', handler: second },
       ];`,
    ],
    [
      'a spread-first duplicate entry',
      `function second() {}
       const routes = [
         { ...base, path: '/users', method: 'GET', handler: first },
         { path: '/users', method: 'GET', handler: second },
       ];`,
    ],
  ])('tombstones %s', (_name, table) => {
    expect(compact(`${table} ${dispatch('routes')}`)).toEqual([]);
  });

  it('rejects shadowed table and loop-entry bindings', () => {
    expect(
      compact(`
        const routes = [{ path: '/outer', method: 'GET', handler: outer }];
        {
          const routes = [{ path: '/inner', method: 'GET', handler: inner }];
          for (const route of routes) {
            if (route.path === request.path && route.method === request.method) route.handler();
          }
        }
      `),
    ).toEqual([]);

    expect(
      compact(`
        const routes = [{ path: '/outer', method: 'GET', handler: outer }];
        for (const route of routes) {
          {
            const route = fakeRoute;
            if (route.path === request.path && route.method === request.method) route.handler();
          }
        }
      `),
    ).toEqual([]);
  });

  it('suppresses a named import shadowed anywhere in the file', () => {
    const detections = JAVASCRIPT_HTTP_PLUGIN.scan(
      jsParser.parse(`
        import { listUsers } from './handlers.js';
        function wrapper(listUsers) { return listUsers; }
        const routes = [{ path: '/users', method: 'GET', handler: listUsers }];
        ${dispatch('routes')}
      `),
    );
    expect(detections).not.toContainEqual(
      expect.objectContaining({ framework: DATA_ROUTE_TABLE_SOURCE, path: '/users' }),
    );
  });

  it('feeds the Node group scanner with named-import provenance', () => {
    const detections = JAVASCRIPT_HTTP_PLUGIN.scan(
      jsParser.parse(`
        import { listUsers as handleUsers } from './handlers.js';
        const routes = [{ path: '/users', method: 'GET', handler: handleUsers }];
        ${dispatch('routes')}
      `),
    );
    expect(detections).toContainEqual({
      role: 'provider',
      framework: DATA_ROUTE_TABLE_SOURCE,
      method: 'GET',
      path: '/users',
      name: 'listUsers',
      handlerImport: { name: 'listUsers', module: './handlers.js' },
      strictHandlerResolution: true,
      line: 3,
      confidence: 0.8,
    });
  });
});
