import { test, expect, afterAll } from 'vitest';
import { writeFileSync, rmSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { transform } from '../plugin/compile/transformer.ts';

const HERE = dirname(fileURLToPath(import.meta.url));
// Write gen files directly in the test dir so relative imports `../src/...`
// resolve to serializer/src/index.ts.
const GEN_DIR = HERE;

let counter = 0;
async function transformAndImport(source: string): Promise<Record<string, unknown>> {
  const id = ++counter;
  const file = join(GEN_DIR, `__gen_${id}.ts`);
  const result = transform(source, file, {
    importPath: '../plugin/index.ts',
    packageAliases: ['../plugin/index.ts'],
  });
  writeFileSync(file, result.code, 'utf8');
  // Use file URL + @vite-ignore so vite passes through to native dynamic import.
  const url = `${pathToFileURL(file).href}?t=${Date.now()}`;
  const mod = await import(/* @vite-ignore */ url);
  return mod as Record<string, unknown>;
}

test('transformer: flat type round-trip', async () => {
  const src = `
import { type, u53, f64, str } from '../plugin/index.ts';

export const Ticker = type('TxTicker', {
  symbol: str,
  last: f64,
  volume: f64,
});
`;
  const mod = await transformAndImport(src);
  const Ticker = mod.Ticker as { encode: (v: unknown) => Uint8Array; decode: (b: Uint8Array) => unknown };
  const v = { symbol: 'BTC-USD', last: 100.5, volume: 1234.5 };
  expect(Ticker.decode(Ticker.encode(v))).toEqual(v);
});

test('transformer: nested object via local reference', async () => {
  const src = `
import { type, u53, f64 } from '../plugin/index.ts';

export const Price = type('TxPrice', { value: f64, scale: u53 });
export const Order = type('TxOrder', { id: u53, price: Price, qty: f64 });
`;
  const mod = await transformAndImport(src);
  const Order = mod.Order as { encode: (v: unknown) => Uint8Array; decode: (b: Uint8Array) => unknown };
  const v = { id: 42, price: { value: 100.5, scale: 2 }, qty: 0.5 };
  expect(Order.decode(Order.encode(v))).toEqual(v);
});

test('transformer: combinators (list, opt, enumOf, flags, tuple)', async () => {
  const src = `
import { type, u53, f64, str, list, opt, enumOf, flags, tuple } from '../plugin/index.ts';

export const Combo = type('TxCombo', {
  tags: list(str),
  maybe: opt(f64),
  side: enumOf(['buy', 'sell'] as const),
  f: flags(['ioc', 'post_only'] as const),
  point: tuple(f64, f64),
});
`;
  const mod = await transformAndImport(src);
  const Combo = mod.Combo as { encode: (v: unknown) => Uint8Array; decode: (b: Uint8Array) => unknown };
  const v = {
    tags: ['a', 'b'],
    maybe: 3.14,
    side: 'buy',
    f: { ioc: true, post_only: false },
    point: [1, 2],
  };
  expect(Combo.decode(Combo.encode(v))).toEqual(v);
});

test('transformer: anonymous (no name) — uses const name as schema name', async () => {
  const src = `
import { type, u53, f64 } from '../plugin/index.ts';

export const TxAnon = type({ x: u53, y: f64 });
`;
  const mod = await transformAndImport(src);
  const T = mod.TxAnon as {
    encode: (v: unknown) => Uint8Array;
    decode: (b: Uint8Array) => unknown;
    id: number;
    name: string;
  };
  expect(T.name).toBe('TxAnon');
  const v = { x: 1, y: 2.5 };
  expect(T.decode(T.encode(v))).toEqual(v);
});

test('transformer: array of nested objects (the OrderBook hot path)', async () => {
  const src = `
import { type, u53, f64, str, list } from '../plugin/index.ts';

export const Level = type('TxLevel', { p: f64, q: f64 });
export const Book = type('TxBook', {
  symbol: str,
  ts: u53,
  bids: list(Level),
  asks: list(Level),
});
`;
  const mod = await transformAndImport(src);
  const Book = mod.Book as { encode: (v: unknown) => Uint8Array; decode: (b: Uint8Array) => unknown };
  const v = {
    symbol: 'BTC-USD',
    ts: 1700000000000,
    bids: Array.from({ length: 100 }, (_, i) => ({ p: 100 - i * 0.1, q: 0.5 + i * 0.01 })),
    asks: Array.from({ length: 100 }, (_, i) => ({ p: 100 + i * 0.1, q: 0.5 + i * 0.01 })),
  };
  const decoded = Book.decode(Book.encode(v)) as typeof v;
  expect(decoded.symbol).toBe(v.symbol);
  expect(decoded.ts).toBe(v.ts);
  expect(decoded.bids.length).toBe(100);
  expect(decoded.bids[0]).toEqual(v.bids[0]);
  expect(decoded.asks[99]).toEqual(v.asks[99]);
});

test('transformer: file without type() imports — unchanged', () => {
  const src = `
import { foo } from 'somewhere';
const x = foo();
`;
  const result = transform(src, 'test.ts', { importPath: '../plugin/index.ts' });
  expect(result.transformedCount).toBe(0);
  expect(result.code).toBe(src);
});

test('transformer: file with type import but no calls — adds nothing', () => {
  const src = `
import { type, u53 } from '../plugin/index.ts';
// no type() call here
const x = 1;
`;
  const result = transform(src, 'test.ts', {
    importPath: '../plugin/index.ts',
    packageAliases: ['../plugin/index.ts'],
  });
  expect(result.transformedCount).toBe(0);
});

test('transformer: replaces call with IIFE (smoke check on output)', () => {
  const src = `
import { type, u53, f64 } from '../plugin/index.ts';
export const T = type('TxSmoke', { x: u53, y: f64 });
`;
  const result = transform(src, 'test.ts', {
    importPath: '../plugin/index.ts',
    packageAliases: ['../plugin/index.ts'],
  });
  expect(result.transformedCount).toBe(1);
  expect(result.code).toContain('function encode_TxSmoke');
  expect(result.code).toContain('function decode_TxSmoke');
  expect(result.code).not.toContain("type('TxSmoke'");
});

test('transformer: class with [Serializable] — decoder returns class instances', async () => {
  const src = `
import { type, Serializable, registerClass, f64, enumOf } from '../plugin/index.ts';

export class TxPos {
  side;
  qty;
  entryPrice;

  static [Serializable] = type('TxPos', {
    side: enumOf(['long', 'short']),
    qty: f64,
    entryPrice: f64,
  });

  get notional() { return this.qty * this.entryPrice; }
}

export const TxPosCodec = registerClass(TxPos);
`;
  const mod = await transformAndImport(src);
  const TxPos = mod.TxPos as new () => {
    side: 'long' | 'short';
    qty: number;
    entryPrice: number;
    notional: number;
  };
  const codec = mod.TxPosCodec as {
    encode: (v: unknown) => Uint8Array;
    decode: (b: Uint8Array) => { side: 'long' | 'short'; qty: number; entryPrice: number };
  };

  const v = { side: 'long' as const, qty: 2, entryPrice: 100 };
  const bytes = codec.encode(v);
  const back = codec.decode(bytes);

  // Decoded value must be an actual TxPos instance — methods/getters work.
  expect(back instanceof TxPos).toBe(true);
  expect((back as InstanceType<typeof TxPos>).notional).toBe(200);
  expect(back.side).toBe('long');
  expect(back.qty).toBe(2);
});

test('transformer: registerClass(X) is rewritten to X[Serializable]', () => {
  const src = `
import { type, Serializable, registerClass, f64 } from '../plugin/index.ts';

export class TxBox {
  static [Serializable] = type('TxBox', { v: f64 });
}

export const TxBoxCodec = registerClass(TxBox);
`;
  const result = transform(src, 'test.ts', {
    importPath: '../plugin/index.ts',
    packageAliases: ['../plugin/index.ts'],
  });
  expect(result.code).toContain('TxBox[Serializable]');
  expect(result.code).not.toContain('registerClass(TxBox)');
});

afterAll(() => {
  for (let i = 1; i <= counter; i++) {
    const file = join(GEN_DIR, `__gen_${i}.ts`);
    if (existsSync(file)) rmSync(file, { force: true });
  }
});
