import { describe, expect, it } from 'vitest';
import { matchOcrRule, parseOcrBill, detectSuspiciousAmount } from '../src/domain/pipeline/ocr';
import { MockAiVisionProvider, MockOcrEngine, OcrProcessor } from '../src/domain/pipeline/ocrProcessor';

describe('ocr - matchOcrRule', () => {
  it('微信支付规则匹配', () => {
    const text = '微信支付 金额 24.50 商户:星巴克 2026-07-13 14:30';
    const result = matchOcrRule(text, 'com.tencent.mm');
    expect(result.matched).toBe(true);
    expect(result.ruleName).toBe('微信支付');
    expect(result.event?.amount).toBe('-24.50');
    expect(result.event?.counterparty).toBe('星巴克');
  });

  it('支付宝规则匹配', () => {
    const text = '支付宝 收款方:肯德基 金额 35.00 支出';
    const result = matchOcrRule(text, 'com.eg.android.AlipayGphone');
    expect(result.matched).toBe(true);
    expect(result.ruleName).toBe('支付宝');
    expect(result.event?.direction).toBe('expense');
  });

  it('银行卡规则匹配（含尾号）', () => {
    const text = '尾号1234 信用卡 消费 100.50 商户:超市 2026-07-13 10:00';
    const result = matchOcrRule(text);
    expect(result.matched).toBe(true);
    expect(result.ruleName).toBe('银行卡');
    expect(result.event?.memo).toContain('1234');
  });

  it('收入方向识别', () => {
    const text = '微信支付 金额 5000 收款 2026-07-13 14:30';
    const result = matchOcrRule(text);
    expect(result.matched).toBe(true);
    expect(result.event?.direction).toBe('income');
    expect(result.event?.amount).toBe('5000');  // 收入为正
  });

  it('退款方向识别', () => {
    const text = '支付宝 金额 30 退款 2026-07-13 14:30';
    const result = matchOcrRule(text);
    expect(result.event?.direction).toBe('refund');
  });

  it('无金额不匹配', () => {
    const text = '微信支付 商户:星巴克';
    const result = matchOcrRule(text);
    expect(result.matched).toBe(false);
  });

  it('非账单内容不匹配', () => {
    const result = matchOcrRule('今天天气真好');
    expect(result.matched).toBe(false);
  });

  it('千分位金额识别', () => {
    const text = '微信支付 金额 1,234.50 商户:星巴克 2026-07-13 14:30';
    const result = matchOcrRule(text);
    expect(result.matched).toBe(true);
    expect(result.event?.amount).toBe('-1234.50');
  });

  it('英文商户名空格保留', () => {
    const text = '微信支付 金额 24.50 商户:Starbucks Coffee 2026-07-13 14:30';
    const result = matchOcrRule(text);
    expect(result.matched).toBe(true);
    expect(result.event?.counterparty).toBe('Starbucks Coffee');
  });

  it('中文年月日及缺失年份时间识别', () => {
    const text = '微信支付 金额 24.50 商户:星巴克 7月13日 14:30';
    const result = matchOcrRule(text);
    expect(result.matched).toBe(true);
    const thisYear = new Date().getFullYear();
    expect(result.event?.occurredAt).toBe(`${thisYear}-07-13 14:30`);
  });

  it('向你转账推断为收入', () => {
    const text = '微信支付 金额 100 张三向你转账 2026-07-13 14:30';
    const result = matchOcrRule(text);
    expect(result.matched).toBe(true);
    expect(result.event?.direction).toBe('income');
    expect(result.event?.amount).toBe('100');
  });
});

describe('ocr - parseOcrBill 退化解析', () => {
  it('规则匹配命中时返回规则结果', () => {
    const event = parseOcrBill('微信支付 金额 10 商户:测试');
    expect(event?.direction).toBe('expense');
  });

  it('退化：提取金额 + 推断方向', () => {
    const event = parseOcrBill('您有一笔 50元 消费');
    expect(event).not.toBeNull();
    expect(event?.amount).toBe('-50');
    expect(event?.direction).toBe('expense');
  });

  it('无任何金额返回 null', () => {
    expect(parseOcrBill('纯文本无金额')).toBeNull();
  });

  it('退化分支：非支付App包名保留在 raw 中', () => {
    const event = parseOcrBill('消费 ￥100', 'com.test.app');
    expect(event).not.toBeNull();
    expect(event?.raw.packageName).toBe('com.test.app');
  });

  it('退化分支：拦截支付App包名以防止列表页误匹配', () => {
    const event1 = parseOcrBill('消费 ￥100', 'com.eg.android.AlipayGphone');
    expect(event1).toBeNull();

    const event2 = parseOcrBill('付款 50元', 'com.tencent.mm');
    expect(event2).toBeNull();
  });

  it('退化分支：未知包名原样保留', () => {
    const event = parseOcrBill('支付 ￥9.9', 'com.unknown.app');
    expect(event?.raw.packageName).toBe('com.unknown.app');
  });

  it('退化分支：无 packageName 时为空字符串', () => {
    const event = parseOcrBill('支付 ￥9.9');
    expect(event?.raw.packageName).toBe('');
  });
});

describe('ocr - 账单详情页识别', () => {
  // 本次真机实测的抖音月付还款账单详情页（| 分隔格式）
  const douyinCase = '中工 | d | 突圳市中融小额贷款有限公司 | -8.51 | 交易成功 | 支付时间 | 2026-07-15.09:01:18 | 付款方式 | 兴业银行储蓄卡（2586） | 商品说明 | 抖音月付 | 支付奖励 | 立即领取10积分 | 收款万全称 | 深圳市中融小额贷款有限公司 | 订单号 | 2026071523001442621427676501 | 商家订单号 | 200107260715010284934083340 | 账单管理 | 账单分类 | 信用借还 | 标签 | 请选择 | 为您推荐 | 贷款十 | 计入收支 | 备注 | 添加';

  it('抖音月付详情页：识别成功，金额 -8.51', () => {
    const event = parseOcrBill(douyinCase, 'com.eg.android.AlipayGphone');
    expect(event).not.toBeNull();
    expect(event?.amount).toBe('-8.51');
    expect(event?.direction).toBe('expense');
    expect(event?.raw.packageName).toBe('com.eg.android.AlipayGphone');
  });

  it('详情页：提取付款方式到 raw（供账户解析）', () => {
    const event = parseOcrBill(douyinCase, 'com.eg.android.AlipayGphone');
    expect(event?.raw['收/付款方式']).toBe('兴业银行储蓄卡（2586）');
  });

  it('详情页：提取支付时间（日期时间用 . 连接的格式）', () => {
    const event = parseOcrBill(douyinCase, 'com.eg.android.AlipayGphone');
    expect(event?.occurredAt).toBe('2026-07-15 09:01');
  });

  it('详情页：提取商品说明', () => {
    const event = parseOcrBill(douyinCase, 'com.eg.android.AlipayGphone');
    expect(event?.counterparty).toBe('抖音月付');
  });

  it('详情页：换行分隔格式也能识别', () => {
    const text = '交易详情\n-15.00\n交易成功\n付款方式 余额宝\n商品说明 星巴克咖啡\n支付时间 2026-07-14 10:30:00\n订单号 20260714103000123456';
    const event = parseOcrBill(text, 'com.eg.android.AlipayGphone');
    expect(event).not.toBeNull();
    expect(event?.amount).toBe('-15.00');
    expect(event?.counterparty).toBe('星巴克咖啡');
    expect(event?.raw['收/付款方式']).toBe('余额宝');
  });

  it('详情页：金额不误抓日期时间里的数字', () => {
    // 2026-07-15.09 不应被误认为 -15.09
    const event = parseOcrBill(douyinCase, 'com.eg.android.AlipayGphone');
    expect(event?.amount).toBe('-8.51');  // 而非 -15.09
  });

  it('详情页：正数金额识别为收入', () => {
    const text = '退款详情\n+88.88\n退款成功\n付款方式 花呗\n商品说明 商品退款\n支付时间 2026-07-14 10:30:00';
    const event = parseOcrBill(text, 'com.eg.android.AlipayGphone');
    expect(event?.amount).toBe('88.88');
    expect(event?.direction).toBe('income');
  });

  it('详情页：无小数点但有正负号金额识别', () => {
    const text = '交易详情\n+7540\n交易成功\n付款方式 余额宝\n商品说明 收益转账\n支付时间 2026-07-14 10:30:00';
    const event = parseOcrBill(text, 'com.eg.android.AlipayGphone');
    expect(event?.amount).toBe('7540');
    expect(event?.direction).toBe('income');
  });

  it('详情页：交易对手提取与后缀清理', () => {
    // 微信商品+订单号后缀
    const text1 = '交易详情\n-15.50\n支付时间 2026-07-15 19:39:58\n商品 京东-订单编号356043400524804\n付款方式 零钱';
    const event1 = parseOcrBill(text1, 'com.tencent.mm');
    expect(event1?.counterparty).toBe('京东');

    // 支付宝对方账户
    const text2 = '交易成功\n+7540\n对方账户 广州市弘智服饰有限公司\n创建时间 2026-07-15 20:13:14\n付款方式 余额宝';
    const event2 = parseOcrBill(text2, 'com.eg.android.AlipayGphone');
    expect(event2?.counterparty).toBe('广州市弘智服饰有限公司');
  });

  it('详情页：支持中文年月日时间格式及补零', () => {
    // 包含中文年月日，且没有空格隔开时间
    const text = '付款方：黄*青\n50.00\n支付时间 2026年7月13日20:29:46\n商品 收款\n付款方式 余额';
    const event = parseOcrBill(text, 'com.eg.android.AlipayGphone');
    expect(event).not.toBeNull();
    // 验证转换正确，月/日/时/分格式和空格规范化
    expect(event?.occurredAt).toBe('2026-07-13 20:29');
  });

  it('详情页：日期与时间被换行符分隔时不截断（回归：时间部分丢失）', () => {
    // 真机日志：OCR 把"延迟扣费2026年6月27日 13:30:15"识别成日期与时间被换行分隔
    // 修复前：[^\s|，,]+ 会截断到 2026-06-27，时间丢失
    const text = '云闪付交易详情\n-1.16\n交易成功\n付款方式 招商银行银联信用卡[1589]\n扣款时间 延迟扣费2026年6月27日\n13:30:15\n订单金额 ¥1.40';
    const event = parseOcrBill(text, 'com.unionpay');
    expect(event).not.toBeNull();
    expect(event?.occurredAt).toBe('2026-06-27 13:30');
  });

  it('云闪付详情页：提取"公司(乘车码)"模式商户名', () => {
    // 云闪付乘车码详情页：商户名裸露，无标准标签（商品说明/收款方全称）
    const text = '云闪付交易详情\n长沙公交智慧大数据科技有限公司(乘车码)\n-1.16\n交易成功\n付款方式 招商银行银联信用卡[1589]\n扣款时间 2026年6月27日 13:30:15\n订单金额 ¥1.40';
    const event = parseOcrBill(text, 'com.unionpay');
    expect(event).not.toBeNull();
    expect(event?.amount).toBe('-1.16');
    expect(event?.counterparty).toContain('长沙公交');
  });

  it('详情页：金额 OCR 常见符号错识纠错（如 898-34 -> 898.34）', () => {
    // 模拟还款成功页面，金额中的点被错识为减号或者逗号
    const text1 = '还款成功\n898-34\n付款方式 余额\n商品说明 还款\n创建时间 2026-07-13 17:57:45';
    const event1 = parseOcrBill(text1, 'com.eg.android.AlipayGphone');
    expect(event1).not.toBeNull();
    expect(event1?.amount).toBe('-898.34'); // 应当成功纠错为小数点并记为支出

    const text2 = '交易成功\n1234,56\n付款方式 余额\n商品说明 交易\n创建时间 2026-07-13 17:57:45';
    const event2 = parseOcrBill(text2, 'com.eg.android.AlipayGphone');
    expect(event2?.amount).toBe('-1234.56'); // 应当成功将逗号规范化为小数点

    const text3 = '还款成功\n1.898.34\n付款方式 余额\n商品说明 还款\n创建时间 2026-07-13 17:57:45';
    const event3 = parseOcrBill(text3, 'com.eg.android.AlipayGphone');
    expect(event3).not.toBeNull();
    expect(event3?.amount).toBe('-1898.34'); // 应当成功将千分位点号规范化并保留小数位点号
  });

  it('误判防护：纯订单号页面不识别', () => {
    const text = '订单号 2026071523001442621427676501\n商家订单号 200107260715010284934083340';
    expect(parseOcrBill(text)).toBeNull();
  });

  it('误判防护：仅 1 个特征词不识别（需 ≥2 个）', () => {
    const text = '订单号 这是一个普通页面，只有订单号一个特征词，金额 100';
    // 「金额 100」会被退化解析抓到，但不应走详情页分支
    const event = parseOcrBill(text);
    // 退化解析会命中（有"金额"关键词），验证它走的是退化而非详情页
    expect(event?.memo).toBe('OCR 退化解析');
  });

  it('误判防护：支付App列表页不应被退化解析匹配', () => {
    // 模拟支付宝列表页
    const listTextAlipay = '全部 支出 转账 余额宝-自动转入 75.40 抖音月付 -8.51';
    expect(parseOcrBill(listTextAlipay, 'com.eg.android.AlipayGphone')).toBeNull();

    // 模拟微信列表页
    const listTextWechat = '收支统计 2026年7月 支出￥37032收入¥5651 京东 -15.50';
    expect(parseOcrBill(listTextWechat, 'com.tencent.mm')).toBeNull();
  });

  // ─── 真机日志回归（2026-07-24 微信账单详情页） ───

  it('微信消费详情页：金额负号被 OCR 识别为 en-dash（– U+2013）也能识别', () => {
    // 真机日志：OCR 把 -18.56 的负号识别成了 en-dash –18.56
    const text = '京东 | \u201318.56 | 当前状态 | 支付成功 | 支付时间 | 2026年7月19日19:11:41 | 商品 | 京东-订单编号3564434013915992 | 支付方式 | 零钱 | 交易单号 | 4500000272202607191472165639 | 商户单号 | 6183532607191911310261343162 | 账单服务 | 申请电子凭证 | 联系商家';
    const event = parseOcrBill(text, 'com.tencent.mm');
    expect(event).not.toBeNull();
    expect(event?.amount).toBe('-18.56');  // en-dash 归一化为半角减号
    expect(event?.direction).toBe('expense');
  });

  it('微信消费详情页：半角负号 -12.50 正常识别（对照）', () => {
    // 真机日志：同页面不同截图，负号正常识别为半角
    const text = '京东 | -12.50 | 当前状态 | 支付成功 | 支付时间 | 2026年7月19日11:53:27 | 商品 | 京东-订单编号3564434013806431 | 商户全称 | 江苏京东信息技术有限公司 | 支付方式 | 零钱 | 交易单号 | 45000000202607194998529686 | 账单服务 | 联系商家';
    const event = parseOcrBill(text, 'com.tencent.mm');
    expect(event).not.toBeNull();
    expect(event?.amount).toBe('-12.50');
    expect(event?.direction).toBe('expense');
  });

  it('微信消费详情页：提取「支付方式」作为付款方式（非「付款方式」）', () => {
    // 微信账单详情页用「支付方式」而非「付款方式」，应能提取到「零钱」
    const text = '京东 | -12.50 | 支付成功 | 支付时间 | 2026年7月19日11:53:27 | 商品 | 京东 | 支付方式 | 零钱 | 交易单号 | 45000000202607194998529686';
    const event = parseOcrBill(text, 'com.tencent.mm');
    expect(event).not.toBeNull();
    expect(event?.raw['收/付款方式']).toBe('零钱');  // 应提取到，而非 Assets:Unknown
  });

  it('微信转账详情页：转账类特征词命中，+143.00 识别为收入', () => {
    // 真机日志：转账详情页用「转账说明/转账时间/转账单号/已存入零钱」标签
    const text = '转账-来自颜梓好 | +143.00 | 当前状态 | 已存入零钱 | 转账说明 | chatGPT | 转账时间 | 2026年7月24日17:09:30 | 收款时间 | 2026年7月24日17:09:48 | 转账单号 | 10002026024032540093172 | 账单服务';
    const event = parseOcrBill(text, 'com.tencent.mm');
    expect(event).not.toBeNull();
    expect(event?.amount).toBe('143.00');
    expect(event?.direction).toBe('income');  // + 号 → 收入
  });

  it('微信转账详情页：特征词命中不误伤列表页（列表页仍返回 null）', () => {
    // 列表页「转账」二字不应触发转账详情页判定（需多个转账特征词同时命中）
    const listText = '全部 支出 转账 余额宝-自动转入 75.40';
    expect(parseOcrBill(listText, 'com.tencent.mm')).toBeNull();
  });
});

describe('OcrProcessor 分层处理', () => {
  it('Layer 1 规则命中', async () => {
    const engine = new MockOcrEngine();
    engine.defaultResponse = '微信支付 金额 24.50 商户:星巴克 2026-07-13 14:30';
    const processor = new OcrProcessor(engine);
    const result = await processor.process('img1', 'com.tencent.mm');
    expect(result.layer).toBe('layer1-rule');
    expect(result.event?.amount).toBe('-24.50');
  });

  it('分层处理：详情页应当绕过 Layer 1 规则匹配进入 Layer 2', async () => {
    const engine = new MockOcrEngine();
    // 模拟包含“招商银行信用卡”的详情页文本（容易误触发 Layer 1 银行卡规则）
    engine.defaultResponse = '淘宝闪购 | 15.96 | 交易成功 | 支付时间 2026-07-12 19:19:39 | 付款方式 招商银行信用卡（1589）';
    const processor = new OcrProcessor(engine);
    const result = await processor.process('img-detail', 'com.eg.android.AlipayGphone');
    // 应绕过 layer1-rule 并通过 layer2-ocr (详情页解析) 提取到正确的 -15.96 金额
    expect(result.layer).toBe('layer2-ocr');
    expect(result.event?.amount).toBe('-15.96');
  });

  it('Layer 2 退化解析（规则未命中但有金额）', async () => {
    const engine = new MockOcrEngine();
    engine.defaultResponse = '某App 消费 88元';
    const processor = new OcrProcessor(engine);
    const result = await processor.process('img2');
    expect(result.layer).toBe('layer2-ocr');
    expect(result.event?.amount).toBe('-88');
  });

  it('OCR 返回空文本时标记 ocr-empty（区别于有内容但非账单）', async () => {
    const engine = new MockOcrEngine();
    engine.defaultResponse = '';
    const processor = new OcrProcessor(engine);
    const result = await processor.process('img3');
    expect(result.layer).toBe('none');
    expect(result.skipped).toBe('ocr-empty');
  });

  it('Layer 3 AI Vision 启用时降级', async () => {
    const engine = new MockOcrEngine();
    engine.defaultResponse = '非账单的随机文本';
    const aiProvider = new MockAiVisionProvider({
      id: 'ai-vision', occurredAt: '2026-07-13', amount: '99', currency: 'CNY', direction: 'expense',
      counterparty: 'AI识别', memo: '', raw: {},
    });
    const processor = new OcrProcessor(engine, aiProvider, { layer1Enabled: true, layer2Enabled: true, layer3Enabled: true, landscapeDnd: false });
    const result = await processor.process('img4');
    expect(result.layer).toBe('layer3-ai');
    expect(result.event?.amount).toBe('99');
  });

  it('横屏免打扰跳过', async () => {
    const engine = new MockOcrEngine();
    const processor = new OcrProcessor(engine, undefined, { layer1Enabled: true, layer2Enabled: true, layer3Enabled: false, landscapeDnd: true });
    const result = await processor.process('img5', undefined, true);
    expect(result.skipped).toBe('landscape-dnd');
  });

  it('去重：同一图片不重复处理', async () => {
    const engine = new MockOcrEngine();
    engine.defaultResponse = '微信支付 金额 10 商户:x';
    const processor = new OcrProcessor(engine);
    const r1 = await processor.process('same-image');
    const r2 = await processor.process('same-image');
    expect(r1.layer).toBe('layer1-rule');
    expect(r2.skipped).toBe('dedup');
  });

  it('串行化：处理中再次调用排队处理', async () => {
    const engine = new MockOcrEngine();
    engine.defaultResponse = '微信支付 金额 10 商户:x';
    const processor = new OcrProcessor(engine);
    const p1 = processor.process('img-a');
    const p2 = processor.process('img-b');  // p1 未完成时
    const [r1, r2] = await Promise.all([p1, p2]);
    expect(r1.layer).toBe('layer1-rule');
    expect(r2.layer).toBe('layer1-rule');
    expect(r2.skipped).toBeUndefined();
  });

  it('reset 清除去重状态', async () => {
    const engine = new MockOcrEngine();
    engine.defaultResponse = '微信支付 金额 10 商户:x';
    const processor = new OcrProcessor(engine);
    await processor.process('img-reset');
    processor.reset();
    const result = await processor.process('img-reset');
    expect(result.layer).toBe('layer1-rule');  // reset 后可再次处理
  });
});

describe('detectSuspiciousAmount 可疑金额启发式', () => {
  it('整数大额 + 外币特征 → 可疑（典型 OCR 丢小数点）', () => {
    // 模拟招行 ChatGPT 订阅：实际 ¥143.97，OCR 丢小数点变 ￥14397
    const ocrText = '交易明细 GOOGLE*ChatGpt ￥14397 已入账 交易地金额 21.19 美国';
    const result = detectSuspiciousAmount('14397', ocrText);
    expect(result).not.toBeNull();
    expect(result?.reason).toContain('14397');
    expect(result?.reason).toContain('小数点');
  });

  it('带负号的整数大额 + 外币特征 → 可疑（去符号后判定）', () => {
    const ocrText = '消费 14397 交易地金额 21.19 USD';
    const result = detectSuspiciousAmount('-14397', ocrText);
    expect(result).not.toBeNull();
  });

  it('整数大额但无外币特征 → 不可疑（正常大额整数消费）', () => {
    const ocrText = '微信支付 金额 5000 商户:某商户';
    expect(detectSuspiciousAmount('5000', ocrText)).toBeNull();
  });

  it('有小数点的金额 → 不可疑（金额正常）', () => {
    const ocrText = '交易明细 ￥143.97 交易地金额 21.19 美国';
    expect(detectSuspiciousAmount('143.97', ocrText)).toBeNull();
  });

  it('小整数（<1000）→ 不可疑（即使无小数点）', () => {
    const ocrText = '消费 500 交易地金额 21.19';
    expect(detectSuspiciousAmount('500', ocrText)).toBeNull();
  });

  it('千分位整数大额 + 外币 → 可疑（去逗号后判定）', () => {
    const ocrText = '消费 14,397 交易地 美国';
    const result = detectSuspiciousAmount('14,397', ocrText);
    expect(result).not.toBeNull();
  });
});
