/**
 * Tests for the codegen emitter (`imprint emit`). Verifies the shape of
 * the generated <assetRoot>/<site>/<toolName>/index.ts: WORKFLOW const, Input
 * interface, exported camelCase tool function, runtime import, default
 * fallbacks. Doesn't actually execute the generated tool — that's
 * runtime.test.ts territory.
 */

import { describe, expect, it } from 'bun:test';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join as pathJoin } from 'node:path';
import { emit } from '../src/imprint/emit.ts';
import type { Workflow } from '../src/imprint/types.ts';

function withTempDir<T>(fn: (dir: string) => T): T {
  const dir = mkdtempSync(pathJoin(tmpdir(), 'imprint-emit-test-'));
  try {
    return fn(dir);
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
}

function writeWorkflow(dir: string, w: Workflow): string {
  const path = pathJoin(dir, 'workflow.json');
  writeFileSync(path, JSON.stringify(w, null, 2));
  return path;
}

const MIN_WORKFLOW: Workflow = {
  toolName: 'do_thing',
  intent: { description: 'Do a thing.' },
  parameters: [
    { name: 'q', type: 'string', description: 'query' },
    { name: 'count', type: 'number', description: 'count', default: 10 },
  ],
  requests: [
    {
      method: 'GET',
      url: 'https://example.com/api/x?q=${param.q}&n=${param.count}',
      headers: {},
    },
  ],
  site: 'demo',
};

describe('emit', () => {
  it('writes <assetRoot>/<site>/<toolName>/index.ts with the expected exports', () => {
    withTempDir((dir) => {
      const wfPath = writeWorkflow(dir, MIN_WORKFLOW);
      const result = emit({ workflowPath: wfPath, outDir: dir });

      expect(result.toolName).toBe('do_thing');
      expect(result.outPath.endsWith('/index.ts')).toBe(true);

      const source = readFileSync(result.outPath, 'utf8');

      // Generated header signals codegen + names the regenerate command.
      expect(source).toContain('GENERATED by `imprint emit`');
      expect(source).toContain('imprint emit ~/.imprint/demo/do_thing/workflow.json --force');

      // Workflow object inlined (so the file is self-contained).
      expect(source).toContain('"toolName": "do_thing"');

      // PascalCase Input interface; camelCase tool fn.
      expect(source).toContain('export interface DoThingInput');
      expect(source).toContain('export async function doThing(');

      // Required vs optional params reflect the schema (default → optional).
      expect(source).toMatch(/q: string;/); // required
      expect(source).toMatch(/count\?: number;/); // has default → optional

      // Re-exports the WORKFLOW constant for tool-loader discovery.
      expect(source).toContain('export { WORKFLOW }');
    });
  });

  it('refuses to overwrite an existing index.ts without --force', () => {
    withTempDir((dir) => {
      const wfPath = writeWorkflow(dir, MIN_WORKFLOW);
      emit({ workflowPath: wfPath, outDir: dir });
      expect(() => emit({ workflowPath: wfPath, outDir: dir })).toThrow(/already exists/);
    });
  });

  it('overwrites with --force', () => {
    withTempDir((dir) => {
      const wfPath = writeWorkflow(dir, MIN_WORKFLOW);
      emit({ workflowPath: wfPath, outDir: dir });
      // Sentinel — should be replaced after the second emit.
      const indexPath = pathJoin(dir, 'index.ts');
      const before = readFileSync(indexPath, 'utf8');
      writeFileSync(indexPath, '/* sentinel — should be overwritten */', 'utf8');
      emit({ workflowPath: wfPath, outDir: dir, force: true });
      const after = readFileSync(indexPath, 'utf8');
      expect(after).not.toBe('/* sentinel — should be overwritten */');
      expect(after).toBe(before);
    });
  });

  it('handles a workflow with zero parameters', () => {
    withTempDir((dir) => {
      const wf: Workflow = {
        ...MIN_WORKFLOW,
        toolName: 'no_params',
        parameters: [],
      };
      const wfPath = writeWorkflow(dir, wf);
      const r = emit({ workflowPath: wfPath, outDir: dir });
      const source = readFileSync(r.outPath, 'utf8');
      expect(source).toContain('export interface NoParamsInput');
      // No parameters → empty params block but still typechecks.
      expect(source).toContain('// (no parameters)');
    });
  });

  it("creates the output dir if it doesn't exist", () => {
    withTempDir((dir) => {
      const wfPath = writeWorkflow(dir, MIN_WORKFLOW);
      const target = pathJoin(dir, 'nested', 'output');
      const r = emit({ workflowPath: wfPath, outDir: target });
      expect(r.outPath.startsWith(target)).toBe(true);
      // No throw means the nested dir was created.
    });
  });

  it('defaults output to a tool-name subdirectory', () => {
    withTempDir((dir) => {
      const wfPath = writeWorkflow(dir, MIN_WORKFLOW);
      const r = emit({ workflowPath: wfPath });
      expect(r.outPath).toBe(pathJoin(dir, 'do_thing', 'index.ts'));
    });
  });
});

describe('emit — credential-shape guardrail', () => {
  // Exactly the panw-canteen failure mode: workflow declares
  // userid/password as plain parameters and templates them with
  // `${param.X}` in the LOGIN body. The guardrail must refuse to write.
  it('refuses to emit when password is templated as a plain parameter', () => {
    withTempDir((dir) => {
      const wf: Workflow = {
        toolName: 'load_thing_with_login',
        intent: { description: 'Logs in and loads.' },
        parameters: [
          { name: 'store_id', type: 'string', description: 'store id', default: 'STORE01' },
          { name: 'userid', type: 'string', description: 'email/username' },
          { name: 'password', type: 'string', description: 'password' },
        ],
        requests: [
          {
            method: 'POST',
            url: 'https://example.com/api/execute',
            headers: { 'Content-Type': 'text/plain' },
            body: JSON.stringify({
              name: 'LOGIN',
              locationid: '${param.store_id}',
              LOGIN: { userid: '${param.userid}', password: '${param.password}' },
            }),
          },
        ],
        site: 'demo-creds',
      };
      const wfPath = writeWorkflow(dir, wf);
      expect(() => emit({ workflowPath: wfPath, outDir: dir })).toThrow(
        /credential-shaped parameter/,
      );
    });
  });

  it('refuses to emit even when only the username half is parameterized', () => {
    withTempDir((dir) => {
      const wf: Workflow = {
        toolName: 'login_only',
        intent: { description: 'Logs in.' },
        parameters: [{ name: 'email', type: 'string', description: 'email' }],
        requests: [
          {
            method: 'POST',
            url: 'https://example.com/login',
            headers: {},
            body: 'email=${param.email}&password=${param.password_unused}',
          },
        ],
        site: 'demo-creds',
      };
      const wfPath = writeWorkflow(dir, wf);
      expect(() => emit({ workflowPath: wfPath, outDir: dir })).toThrow(
        /credential-shaped parameter/,
      );
    });
  });

  it('allows credential-store-templated workflows through', () => {
    // The correct pattern: same login flow but using `${credential.X}`.
    // `userid`/`password` are not even declared as parameters — they come
    // from the credential store. This must emit cleanly.
    withTempDir((dir) => {
      const wf: Workflow = {
        toolName: 'load_thing_with_login_correct',
        intent: { description: 'Logs in and loads.' },
        parameters: [
          { name: 'store_id', type: 'string', description: 'store id', default: 'STORE01' },
        ],
        requests: [
          {
            method: 'POST',
            url: 'https://example.com/api/execute',
            headers: {},
            body: JSON.stringify({
              name: 'LOGIN',
              locationid: '${param.store_id}',
              LOGIN: {
                userid: '${credential.userid}',
                password: '${credential.password}',
              },
            }),
          },
        ],
        site: 'demo-creds',
      };
      const wfPath = writeWorkflow(dir, wf);
      const r = emit({ workflowPath: wfPath, outDir: dir });
      const source = readFileSync(r.outPath, 'utf8');
      // userid/password should NOT appear as Input fields since they
      // aren't workflow parameters.
      expect(source).not.toMatch(/userid\??: string/);
      expect(source).not.toMatch(/password\??: string/);
    });
  });

  it('allows credential-shaped param names when both ${param} and ${credential} are present', () => {
    // Edge case: some workflows expose userid as an optional override
    // alongside ${credential.userid} fallback in the body. Today this
    // isn't an enforced pattern — but it's a defensible choice and the
    // guardrail shouldn't block it. We require at least one credential
    // reference in the workflow to mark intent.
    withTempDir((dir) => {
      const wf: Workflow = {
        toolName: 'load_with_override',
        intent: { description: 'Logs in with optional override.' },
        parameters: [{ name: 'userid', type: 'string', description: 'override', default: '' }],
        requests: [
          {
            method: 'POST',
            url: 'https://example.com/api',
            headers: {},
            body: JSON.stringify({
              userid: '${param.userid}',
              fallback: '${credential.userid}',
            }),
          },
        ],
        site: 'demo-creds',
      };
      const wfPath = writeWorkflow(dir, wf);
      expect(() => emit({ workflowPath: wfPath, outDir: dir })).not.toThrow();
    });
  });

  it('allows non-credential parameters with familiar names through', () => {
    // `store_id`, `account_number` (not credential-shaped), `email` only
    // in URL paths, etc. shouldn't trip the guardrail.
    withTempDir((dir) => {
      const wf: Workflow = {
        toolName: 'plain_search',
        intent: { description: 'Searches.' },
        parameters: [
          { name: 'store_id', type: 'string', description: 'store id' },
          { name: 'q', type: 'string', description: 'query' },
        ],
        requests: [
          {
            method: 'GET',
            url: 'https://example.com/search?store=${param.store_id}&q=${param.q}',
            headers: {},
          },
        ],
        site: 'demo-search',
      };
      const wfPath = writeWorkflow(dir, wf);
      expect(() => emit({ workflowPath: wfPath, outDir: dir })).not.toThrow();
    });
  });
});
