import { describe, expect, it } from 'vitest';
import { hash, parseLedger, serializeTransaction, validateTransaction } from '../src/domain/core/ledger';

const parse = (content: string, path = 'main.bean'): ReturnType<typeof parseLedger> =>
  parseLedger([{ path, content }]);

const fixture = `option "title" "测试账本"
option "operating_currency" "CNY"
2026-01-01 open Assets:Alipay CNY
2026-01-01 open Expenses:Food CNY
2026-01-01 open Expenses:Uncategorized CNY
2026-01-01 open Income:Uncategorized CNY
2026-01-01 open Assets:Stocks STOCK
2026-01-31 balance Assets:Alipay 100.00 CNY
2026-02-01 * "咖啡店" "冰美式" #food #lunch
  bm-source: "ocr"
  Assets:Alipay  -24.50 CNY
  Expenses:Food   24.50 CNY
2026-02-02 * "工资" "月薪"
  Assets:Alipay   5000.00 CNY
  Income:Uncategorized  -5000.00 CNY
2026-02-03 * "买入股票" "建仓"
  Assets:Alipay   -1200.00 CNY
  Assets:Stocks  10.00 STOCK {120.00 CNY}
`;

describe('parseLedger - 指令解析', () => {
  it('解析 open 账户（含币种）', () => {
    const ledger = parse(fixture);
    expect(ledger.accounts.get('Assets:Alipay')?.currencies).toEqual(['CNY']);
    expect(ledger.accounts.get('Assets:Stocks')?.currencies).toEqual(['STOCK']);
  });

  it('解析 close 账户（先 open 后 close）', () => {
    const ledger = parse('2026-01-01 open Assets:Old CNY\n2026-01-15 close Assets:Old\n');
    expect(ledger.accounts.get('Assets:Old')?.closedOn).toBe('2026-01-15');
  });

  it('close 未开户账户时记 diagnostic', () => {
    const ledger = parse('2026-01-15 close Assets:NeverOpened\n');
    expect(ledger.diagnostics).toHaveLength(1);
    expect(ledger.diagnostics[0].message).toContain('未开户');
  });

  it('解析 balance 断言到 balances', () => {
    const ledger = parse(fixture);
    expect(ledger.balances).toHaveLength(1);
    expect(ledger.balances[0]).toMatchObject({ account: 'Assets:Alipay', amount: '100.00', currency: 'CNY' });
  });

  it('解析 option 到 options', () => {
    const ledger = parse(fixture);
    const title = ledger.options.find(o => o.key === 'title');
    expect(title?.value).toBe('测试账本');
    expect(ledger.options.find(o => o.key === 'operating_currency')?.value).toBe('CNY');
  });

  it('plugin 指令记入 unsupported', () => {
    const ledger = parse('plugin "beancount.plugins.auto_accounts"\n');
    expect(ledger.unsupported).toHaveLength(1);
    expect(ledger.unsupported[0].message).toContain('plugin');
  });

  it('不认识的日期型指令记入 unsupported（不阻断解析）', () => {
    const ledger = parse('2026-01-01 commodity CNY\n2026-01-02 custom "budget" "Food" 100.00\n2026-02-01 * "x" "y"\n  Assets:Alipay  -1 CNY\n  Expenses:Food  1 CNY\n');
    expect(ledger.unsupported.length).toBeGreaterThanOrEqual(2);
    expect(ledger.transactions).toHaveLength(1);
  });
});

describe('parseLedger - 交易与 posting', () => {
  it('解析 payee + narration + tag', () => {
    const ledger = parse(fixture);
    const tx = ledger.transactions.find(t => t.narration === '冰美式')!;
    expect(tx.payee).toBe('咖啡店');
    expect(tx.tags).toEqual(['food', 'lunch']);
  });

  it('解析交易级 metadata', () => {
    const ledger = parse(fixture);
    const tx = ledger.transactions.find(t => t.narration === '冰美式')!;
    expect(tx.metadata?.['bm-source']).toBe('ocr');
  });

  it('解析 posting cost {...}', () => {
    const ledger = parse(fixture);
    const tx = ledger.transactions.find(t => t.narration === '建仓')!;
    const stockPosting = tx.postings.find(p => p.account === 'Assets:Stocks')!;
    expect(stockPosting.cost).toBe('120.00 CNY');
  });

  it('解析无金额的 elided posting', () => {
    const ledger = parse('2026-01-01 open Assets:Cash CNY\n2026-01-01 open Expenses:Food CNY\n2026-02-01 * "x" "y"\n  Assets:Cash  -10 CNY\n  Expenses:Food\n');
    const tx = ledger.transactions[0];
    expect(tx.postings).toHaveLength(2);
    expect(tx.postings[1].account).toBe('Expenses:Food');
    expect(tx.postings[1].amount).toBeUndefined();
  });

  it('解析多行注释与空行不被误判', () => {
    const ledger = parse('; 这是一个注释\n\n2026-01-01 open Assets:Cash CNY\n* 这也是注释\n');
    expect(ledger.accounts.size).toBe(1);
  });
});

describe('validateTransaction', () => {
  const ledger = parse(fixture);

  it('精确小数 + 平衡校验通过', () => {
    const result = validateTransaction({
      date: '2026-02-01', narration: '午餐',
      postings: [{ account: 'Assets:Alipay', amount: '-0.1', currency: 'CNY' }, { account: 'Expenses:Food', amount: '0.1', currency: 'CNY' }],
    }, ledger);
    expect(result.valid).toBe(true);
  });

  it('不平衡时报错', () => {
    const result = validateTransaction({
      date: '2026-02-01', narration: '错误',
      postings: [{ account: 'Assets:Alipay', amount: '-1', currency: 'CNY' }, { account: 'Expenses:Food', amount: '0.9', currency: 'CNY' }],
    }, ledger);
    expect(result.errors[0]).toContain('不平衡');
  });

  it('账户未 open 报错', () => {
    const result = validateTransaction({
      date: '2026-02-01', narration: 'x',
      postings: [{ account: 'Assets:NotExist', amount: '-1', currency: 'CNY' }, { account: 'Expenses:Food', amount: '1', currency: 'CNY' }],
    }, ledger);
    expect(result.errors.some(e => e.includes('未 open'))).toBe(true);
  });

  it('已关闭账户报错', () => {
    const localLedger = parse('2026-01-01 open Assets:Old CNY\n2026-01-15 close Assets:Old\n2026-01-01 open Expenses:Food CNY\n');
    const result = validateTransaction({
      date: '2026-02-01', narration: 'x',
      postings: [{ account: 'Assets:Old', amount: '-1', currency: 'CNY' }, { account: 'Expenses:Food', amount: '1', currency: 'CNY' }],
    }, localLedger);
    expect(result.errors.some(e => e.includes('已关闭'))).toBe(true);
  });

  it('日期格式校验', () => {
    const result = validateTransaction({
      date: '2026/02/01', narration: 'x',
      postings: [{ account: 'Assets:Alipay', amount: '-1', currency: 'CNY' }, { account: 'Expenses:Food', amount: '1', currency: 'CNY' }],
    }, ledger);
    expect(result.errors.some(e => e.includes('YYYY-MM-DD'))).toBe(true);
  });
});

describe('serializeTransaction', () => {
  it('序列化 payee + narration + tag', () => {
    const out = serializeTransaction({
      date: '2026-02-01', payee: '咖啡店', narration: '冰美式', tags: ['food'],
      postings: [{ account: 'Assets:Alipay', amount: '-24.5', currency: 'CNY' }, { account: 'Expenses:Food', amount: '24.5', currency: 'CNY' }],
    });
    expect(out).toContain('"咖啡店" "冰美式" #food');
    expect(out).toContain('Assets:Alipay  -24.5 CNY');
  });

  it('序列化 metadata', () => {
    const out = serializeTransaction({
      date: '2026-02-01', narration: 'x', metadata: { 'bm-source': 'ocr' },
      postings: [{ account: 'Assets:Alipay', amount: '-1', currency: 'CNY' }, { account: 'Expenses:Food', amount: '1', currency: 'CNY' }],
    });
    expect(out).toContain('bm-source: "ocr"');
  });

  it('序列化结果可被解析器重新读回（往返一致）', () => {
    const draft = {
      date: '2026-02-01', payee: '测试', narration: '往返', tags: ['t1'],
      postings: [{ account: 'Assets:Alipay', amount: '-10', currency: 'CNY' }, { account: 'Expenses:Food', amount: '10', currency: 'CNY' }],
    };
    const out = serializeTransaction(draft);
    const ledger = parse(`2026-01-01 open Assets:Alipay CNY\n2026-01-01 open Expenses:Food CNY\n${out}`);
    const tx = ledger.transactions[0];
    expect(tx.payee).toBe('测试');
    expect(tx.narration).toBe('往返');
    expect(tx.tags).toEqual(['t1']);
    expect(tx.postings[0].amount).toBe('-10');
  });

  it('序列化 flag (!) 不丢失', () => {
    const out = serializeTransaction({
      date: '2026-02-01', narration: 'pending', flag: '!',
      postings: [{ account: 'Assets:Alipay', amount: '-10', currency: 'CNY' }, { account: 'Expenses:Food', amount: '10', currency: 'CNY' }],
    });
    expect(out).toContain('2026-02-01 !');
    expect(out).not.toContain('2026-02-01 *');
  });

  it('序列化 links (^tag) 不丢失', () => {
    const out = serializeTransaction({
      date: '2026-02-01', narration: 'linked', links: ['lnk-abc'],
      postings: [{ account: 'Assets:Alipay', amount: '-10', currency: 'CNY' }, { account: 'Expenses:Food', amount: '10', currency: 'CNY' }],
    });
    expect(out).toContain('^lnk-abc');
  });

  it('序列化 cost 和 price 不丢失', () => {
    const out = serializeTransaction({
      date: '2026-02-01', narration: 'investment',
      postings: [{ account: 'Assets:Stocks', amount: '100', currency: 'AAPL', cost: '15000 CNY', price: '150 CNY' }, { account: 'Assets:Bank', amount: '-15000', currency: 'CNY' }],
    });
    expect(out).toContain('{15000 CNY}');
    expect(out).toContain('@ 150 CNY');
  });

  it('编辑往返：flag/links/cost 完整保留', () => {
    const draft = {
      date: '2026-02-01', narration: 'edit test', flag: '!', links: ['lnk-x'],
      postings: [{ account: 'Assets:Alipay', amount: '-10', currency: 'CNY' }, { account: 'Expenses:Food', amount: '10', currency: 'CNY', cost: '10 CNY' }],
    };
    const out = serializeTransaction(draft);
    const ledger = parse(`2026-01-01 open Assets:Alipay CNY\n2026-01-01 open Expenses:Food CNY\n${out}`);
    const tx = ledger.transactions[0];
    expect(tx.flag).toBe('!');
    expect(tx.links).toContain('lnk-x');
    expect(tx.postings[1].cost).toBe('10 CNY');
  });
});

describe('hash（FNV-1a）', () => {
  it('相同输入产生相同 hash', () => {
    expect(hash('abc')).toBe(hash('abc'));
  });
  it('不同输入产生不同 hash', () => {
    expect(hash('abc')).not.toBe(hash('abd'));
  });
  it('hash 为十六进制字符串', () => {
    expect(hash('test')).toMatch(/^[0-9a-f]+$/);
  });
});
