import { describe, it, expect } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  JournalLineNotFoundError,
  InvalidStatusForLineUpdateError,
  AccountNotFoundError,
  AccountNotActiveError,
  InvalidDebitCreditError,
  CurrencyNotFoundError,
} from "../lib/errors.generated";
import {
  baseJournalEntry,
  postedJournalEntry,
  reversedJournalEntry,
  baseJournalLine,
  baseAccount2,
  inactiveAccount,
  baseCurrency,
} from "../testing/fixtures";
import { run } from "./updateJournalLine";

describe("updateJournalLine", () => {
  it("returns error when journal line does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        id: "missing-line",
        accountId: undefined,
        debitAmount: undefined,
        creditAmount: undefined,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    expectErr(result, JournalLineNotFoundError);
  });

  it("returns error when parent journal entry is in POSTED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const postedLine = { ...baseJournalLine, journalEntryId: postedJournalEntry.id };
    spies.select.mockReturnValueOnce(postedLine).mockReturnValueOnce(postedJournalEntry);

    const result = await run(
      db,
      {
        id: postedLine.id,
        accountId: undefined,
        debitAmount: undefined,
        creditAmount: undefined,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    expectErr(result, InvalidStatusForLineUpdateError);
  });

  it("returns error when parent journal entry is in REVERSED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const reversedLine = { ...baseJournalLine, journalEntryId: reversedJournalEntry.id };
    spies.select.mockReturnValueOnce(reversedLine).mockReturnValueOnce(reversedJournalEntry);

    const result = await run(
      db,
      {
        id: reversedLine.id,
        accountId: undefined,
        debitAmount: undefined,
        creditAmount: undefined,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    expectErr(result, InvalidStatusForLineUpdateError);
  });

  it("returns error when updated GL account does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseJournalLine)
      .mockReturnValueOnce(baseJournalEntry)
      .mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: "missing-account",
        debitAmount: undefined,
        creditAmount: undefined,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    expectErr(result, AccountNotFoundError);
  });

  it("returns error when updated GL account is not active", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseJournalLine)
      .mockReturnValueOnce(baseJournalEntry)
      .mockReturnValueOnce(inactiveAccount);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: inactiveAccount.id,
        debitAmount: undefined,
        creditAmount: undefined,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    expectErr(result, AccountNotActiveError);
  });

  it("returns error when updated debit/credit values are invalid", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseJournalLine).mockReturnValueOnce(baseJournalEntry);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: undefined,
        debitAmount: 1000,
        creditAmount: 500,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    expectErr(result, InvalidDebitCreditError);
  });

  it("returns error when updated currency does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseJournalLine)
      .mockReturnValueOnce(baseJournalEntry)
      .mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: undefined,
        debitAmount: undefined,
        creditAmount: undefined,
        description: undefined,
        currencyCode: "missing-currency",
        exchangeRate: 1.5,
      },
      commandCtx,
    );

    expectErr(result, CurrencyNotFoundError);
  });

  it("updates GL account reference on a journal line", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedLine = { ...baseJournalLine, accountId: baseAccount2.id };
    spies.select
      .mockReturnValueOnce(baseJournalLine)
      .mockReturnValueOnce(baseJournalEntry)
      .mockReturnValueOnce(baseAccount2);
    spies.update.mockReturnValueOnce(updatedLine);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: baseAccount2.id,
        debitAmount: undefined,
        creditAmount: undefined,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.journalLine.accountId).toBe(baseAccount2.id);
  });

  it("updates debit amount on a journal line", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedLine = {
      ...baseJournalLine,
      debitAmount: 2000,
      functionalDebitAmount: 2000,
    };
    spies.select.mockReturnValueOnce(baseJournalLine).mockReturnValueOnce(baseJournalEntry);
    spies.update.mockReturnValueOnce(updatedLine);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: undefined,
        debitAmount: 2000,
        creditAmount: null,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.journalLine.debitAmount).toBe(2000);
  });

  it("updates credit amount on a journal line", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedLine = {
      ...baseJournalLine,
      debitAmount: null,
      creditAmount: 1500,
      functionalDebitAmount: null,
      functionalCreditAmount: 1500,
    };
    spies.select.mockReturnValueOnce(baseJournalLine).mockReturnValueOnce(baseJournalEntry);
    spies.update.mockReturnValueOnce(updatedLine);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: undefined,
        debitAmount: null,
        creditAmount: 1500,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.journalLine.creditAmount).toBe(1500);
  });

  it("updates currency and exchange rate on a journal line", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedLine = {
      ...baseJournalLine,
      currencyCode: baseCurrency.code,
      exchangeRate: 1.5,
      functionalDebitAmount: 1500,
    };
    spies.select
      .mockReturnValueOnce(baseJournalLine)
      .mockReturnValueOnce(baseJournalEntry)
      .mockReturnValueOnce(baseCurrency);
    spies.update.mockReturnValueOnce(updatedLine);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: undefined,
        debitAmount: undefined,
        creditAmount: undefined,
        description: undefined,
        currencyCode: baseCurrency.code,
        exchangeRate: 1.5,
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.journalLine.currencyCode).toBe(baseCurrency.code);
    expect(value.journalLine.exchangeRate).toBe(1.5);
  });

  it("recomputes functional currency amount after update", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedLine = {
      ...baseJournalLine,
      debitAmount: 500,
      currencyCode: baseCurrency.code,
      exchangeRate: 2.0,
      functionalDebitAmount: 1000,
    };
    spies.select
      .mockReturnValueOnce(baseJournalLine)
      .mockReturnValueOnce(baseJournalEntry)
      .mockReturnValueOnce(baseCurrency);
    spies.update.mockReturnValueOnce(updatedLine);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: undefined,
        debitAmount: 500,
        creditAmount: null,
        description: undefined,
        currencyCode: baseCurrency.code,
        exchangeRate: 2.0,
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.journalLine).toEqual(updatedLine);
    expect(spies.update).toHaveBeenCalled();
  });

  it("updates line-level description", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedLine = {
      ...baseJournalLine,
      description: "Updated description",
    };
    spies.select.mockReturnValueOnce(baseJournalLine).mockReturnValueOnce(baseJournalEntry);
    spies.update.mockReturnValueOnce(updatedLine);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: undefined,
        debitAmount: undefined,
        creditAmount: undefined,
        description: "Updated description",
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    const value = expectOk(result);
    expect(value.journalLine.description).toBe("Updated description");
  });

  it("emits audit event recording previous and new field values and acting user", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updatedLine = { ...baseJournalLine, debitAmount: 2000, functionalDebitAmount: 2000 };
    spies.select.mockReturnValueOnce(baseJournalLine).mockReturnValueOnce(baseJournalEntry);
    spies.update.mockReturnValueOnce(updatedLine);

    const result = await run(
      db,
      {
        id: baseJournalLine.id,
        accountId: undefined,
        debitAmount: 2000,
        creditAmount: null,
        description: undefined,
        currencyCode: undefined,
        exchangeRate: undefined,
      },
      commandCtx,
    );

    expectOk(result);
    expect(spies.update).toHaveBeenCalled();
  });
});
