import { describe, it, expect, vi, beforeEach } from 'vitest';
import { buildAndSaveTransaction, type TransactionStore } from '../src/domain/transaction/transactionBuilder';
import type { LedgerIndex } from '../src/domain/core/types';

// Mock store
const mockAutoOpenAccounts = vi.fn().mockResolvedValue(undefined);
const mockAddTransaction = vi.fn().mockResolvedValue('tx-123');
let mockLedger: LedgerIndex | null = null;

const createMockStore = (): TransactionStore => ({
  ledger: mockLedger,
  autoOpenAccounts: mockAutoOpenAccounts,
  addTransaction: mockAddTransaction,
});

describe('buildAndSaveTransaction', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockLedger = null;
  });

  describe('amount validation', () => {
    it('throws on empty amount', async () => {
      await expect(
        buildAndSaveTransaction({
          date: '2026-07-18',
          amount: '',
          categoryAccount: 'Expenses:Food',
          sourceAccount: 'Assets:Cash',
          direction: 'expense',
        }, createMockStore())
      ).rejects.toThrow('金额不能为空');
    });

    it('throws on zero amount', async () => {
      await expect(
        buildAndSaveTransaction({
          date: '2026-07-18',
          amount: '0',
          categoryAccount: 'Expenses:Food',
          sourceAccount: 'Assets:Cash',
          direction: 'expense',
        }, createMockStore())
      ).rejects.toThrow('金额数值必须大于 0');
    });

    it('throws on negative amount', async () => {
      await expect(
        buildAndSaveTransaction({
          date: '2026-07-18',
          amount: '-100',
          categoryAccount: 'Expenses:Food',
          sourceAccount: 'Assets:Cash',
          direction: 'expense',
        }, createMockStore())
      ).rejects.toThrow('金额数值必须大于 0');
    });

    it('accepts valid decimal amount', async () => {
      await expect(
        buildAndSaveTransaction({
          date: '2026-07-18',
          amount: '89.30',
          categoryAccount: 'Expenses:Food',
          sourceAccount: 'Assets:Cash',
          direction: 'expense',
        }, createMockStore())
      ).resolves.not.toThrow();
    });
  });

  describe('date normalization', () => {
    it('normalizes YYYY/M/D format', async () => {
      await buildAndSaveTransaction({
        date: '2026/7/18',
        amount: '100',
        categoryAccount: 'Expenses:Food',
        sourceAccount: 'Assets:Cash',
        direction: 'expense',
      }, createMockStore());

      expect(mockAddTransaction).toHaveBeenCalledWith(
        expect.objectContaining({ date: '2026-07-18' }),
        undefined,
        undefined
      );
    });

    it('normalizes YYYY年M月D日 format', async () => {
      await buildAndSaveTransaction({
        date: '2026年7月18日',
        amount: '100',
        categoryAccount: 'Expenses:Food',
        sourceAccount: 'Assets:Cash',
        direction: 'expense',
      }, createMockStore());

      expect(mockAddTransaction).toHaveBeenCalledWith(
        expect.objectContaining({ date: '2026-07-18' }),
        undefined,
        undefined
      );
    });

    it('normalizes YYYY.M.D format', async () => {
      await buildAndSaveTransaction({
        date: '2026.7.18',
        amount: '100',
        categoryAccount: 'Expenses:Food',
        sourceAccount: 'Assets:Cash',
        direction: 'expense',
      }, createMockStore());

      expect(mockAddTransaction).toHaveBeenCalledWith(
        expect.objectContaining({ date: '2026-07-18' }),
        undefined,
        undefined
      );
    });

    it('pads single digit month and day', async () => {
      await buildAndSaveTransaction({
        date: '2026-1-5',
        amount: '100',
        categoryAccount: 'Expenses:Food',
        sourceAccount: 'Assets:Cash',
        direction: 'expense',
      }, createMockStore());

      expect(mockAddTransaction).toHaveBeenCalledWith(
        expect.objectContaining({ date: '2026-01-05' }),
        undefined,
        undefined
      );
    });

    it('throws on invalid date format', async () => {
      await expect(
        buildAndSaveTransaction({
          date: 'invalid',
          amount: '100',
          categoryAccount: 'Expenses:Food',
          sourceAccount: 'Assets:Cash',
          direction: 'expense',
        }, createMockStore())
      ).rejects.toThrow('日期格式不正确');
    });
  });

  describe('posting construction', () => {
    it('creates expense postings correctly', async () => {
      await buildAndSaveTransaction({
        date: '2026-07-18',
        amount: '100',
        categoryAccount: 'Expenses:Food',
        sourceAccount: 'Assets:Cash',
        direction: 'expense',
      }, createMockStore());

      expect(mockAddTransaction).toHaveBeenCalledWith(
        expect.objectContaining({
          postings: [
            { account: 'Assets:Cash', amount: '-100', currency: 'CNY' },
            { account: 'Expenses:Food', amount: '100', currency: 'CNY' },
          ],
        }),
        undefined,
        undefined
      );
    });

    it('creates income postings correctly', async () => {
      await buildAndSaveTransaction({
        date: '2026-07-18',
        amount: '5000',
        categoryAccount: 'Income:Salary',
        sourceAccount: 'Assets:Bank',
        direction: 'income',
      }, createMockStore());

      expect(mockAddTransaction).toHaveBeenCalledWith(
        expect.objectContaining({
          postings: [
            { account: 'Assets:Bank', amount: '5000', currency: 'CNY' },
            { account: 'Income:Salary', amount: '-5000', currency: 'CNY' },
          ],
        }),
        undefined,
        undefined
      );
    });

    it('creates transfer postings correctly', async () => {
      await buildAndSaveTransaction({
        date: '2026-07-18',
        amount: '1000',
        categoryAccount: 'Assets:Bank',
        sourceAccount: 'Assets:Cash',
        direction: 'transfer',
      }, createMockStore());

      expect(mockAddTransaction).toHaveBeenCalledWith(
        expect.objectContaining({
          postings: [
            { account: 'Assets:Cash', amount: '-1000', currency: 'CNY' },
            { account: 'Assets:Bank', amount: '1000', currency: 'CNY' },
          ],
        }),
        undefined,
        undefined
      );
    });
  });

  describe('narration defaults', () => {
    it('uses default narration for expense', async () => {
      await buildAndSaveTransaction({
        date: '2026-07-18',
        amount: '100',
        categoryAccount: 'Expenses:Food',
        sourceAccount: 'Assets:Cash',
        direction: 'expense',
      }, createMockStore());

      expect(mockAddTransaction).toHaveBeenCalledWith(
        expect.objectContaining({ narration: '支出交易' }),
        undefined,
        undefined
      );
    });

    it('uses custom narration when provided', async () => {
      await buildAndSaveTransaction({
        date: '2026-07-18',
        amount: '100',
        categoryAccount: 'Expenses:Food',
        sourceAccount: 'Assets:Cash',
        direction: 'expense',
        narration: '午餐',
      }, createMockStore());

      expect(mockAddTransaction).toHaveBeenCalledWith(
        expect.objectContaining({ narration: '午餐' }),
        undefined,
        undefined
      );
    });
  });

  describe('auto-open accounts', () => {
    it('opens unopened accounts', async () => {
      mockLedger = {
        accounts: new Map([
          ['Assets:Cash', { name: 'Assets:Cash' } as any],
        ]),
      } as unknown as LedgerIndex;

      await buildAndSaveTransaction({
        date: '2026-07-18',
        amount: '100',
        categoryAccount: 'Expenses:Food',
        sourceAccount: 'Assets:Cash',
        direction: 'expense',
      }, createMockStore());

      expect(mockAutoOpenAccounts).toHaveBeenCalledWith(['Expenses:Food']);
    });

    it('opens multiple unopened accounts when ledger is null', async () => {
      mockLedger = null;

      await buildAndSaveTransaction({
        date: '2026-07-18',
        amount: '100',
        categoryAccount: 'Expenses:Food',
        sourceAccount: 'Assets:Cash',
        direction: 'expense',
      }, createMockStore());

      // When ledger is null, autoOpenAccounts is not called (graceful skip)
      expect(mockAutoOpenAccounts).not.toHaveBeenCalled();
    });
  });
});
