import { describe, expect, it, mock } from 'bun:test';
import {
  getRefreshableEarningsPanel,
  getRefreshableFinancialsPanel,
  refreshAffectedEarningsPanels,
  refreshAffectedFinancialsPanels,
  refreshPanelKey,
  shouldRefreshEarningsPanel,
  shouldRefreshFinancialsPanel,
} from './mappingsRefresh';
import type { Workspace } from '../hooks/useTabs';
import type {
  BalanceSheetPanelData,
  EarningsPanelData,
  FinancialsPanelData,
} from '../types/financial';
import type { MappingsUpdatedEvent } from '../types/mappings';
import type { TerminalCommandResponse, WorkspaceTerminalState } from '../types/terminal';

const makeTerminalState = (
  history: WorkspaceTerminalState['history'],
): WorkspaceTerminalState => ({
  history,
  chatSessionId: undefined,
  inputDraft: '',
  isProcessing: false,
  activeRequest: null,
  pendingApproval: null,
  scrollTop: 0,
  stickToBottom: true,
  collapsedThinkingEntryIds: [],
});

const makeEarningsPanel = (
  symbol: string,
  overrides: Partial<EarningsPanelData> = {},
): EarningsPanelData => ({
  symbol,
  companyName: `${symbol} Inc.`,
  cik: `cik-${symbol}`,
  frequency: 'annual',
  latestFiling: {
    accessionNumber: `${symbol}-10k`,
    filingDate: '2025-02-01',
    form: '10-K',
  },
  sourceStatus: {
    companyfactsUsed: false,
  },
  availableConcepts: [],
  availablePeriods: [
    {
      id: '2023',
      label: 'FY2023',
      frequency: 'annual',
      fiscalYear: '2023',
      fiscalPeriod: 'FY',
      periodEnd: '2023-12-31',
      sortKey: '2023',
    },
    {
      id: '2024',
      label: 'FY2024',
      frequency: 'annual',
      fiscalYear: '2024',
      fiscalPeriod: 'FY',
      periodEnd: '2024-12-31',
      sortKey: '2024',
    },
  ],
  selectedPeriodStart: '2023',
  selectedPeriodEnd: '2024',
  selectedCurrency: 'USD',
  availableCurrencies: ['EUR', 'USD'],
  periods: [
    {
      label: 'FY2024',
      displayLabel: 'FY2024',
      periodEnd: '2024-12-31',
      filedDate: '2025-02-01',
      form: '10-K',
      totalRevenues: 100,
    },
    {
      label: 'FY2023',
      displayLabel: 'FY2023',
      periodEnd: '2023-12-31',
      filedDate: '2024-02-01',
      form: '10-K',
      totalRevenues: 90,
    },
  ],
  ...overrides,
});

const makeWorkspace = (
  id: string,
  sourceCommand = '/em AAPL annual 2023 2024',
  panel = makeEarningsPanel('AAPL'),
): Workspace => ({
  id,
  name: id,
  createdAt: new Date('2026-04-13T10:00:00Z'),
  terminal: makeTerminalState([
    {
      id: `${id}-command`,
      type: 'command',
      content: sourceCommand,
      timestamp: new Date('2026-04-13T10:00:00Z'),
    },
    {
      id: `${id}-panel`,
      type: 'panel',
      content: { type: 'earnings', data: panel },
      timestamp: new Date('2026-04-13T10:00:01Z'),
    },
  ]),
});

const makeFinancialsPanel = (
  symbol: string,
  overrides: Partial<FinancialsPanelData> = {},
): FinancialsPanelData => ({
  symbol,
  companyName: `${symbol} Inc.`,
  cik: `cik-${symbol}`,
  frequency: 'annual',
  latestFiling: {
    accessionNumber: `${symbol}-10k`,
    filingDate: '2025-02-01',
    form: '10-K',
  },
  sourceStatus: {
    companyfactsUsed: false,
  },
  availableConcepts: [],
  mappingWarnings: [],
  periods: [
    {
      label: 'FY2024',
      periodEnd: '2024-12-31',
      filedDate: '2025-02-01',
      form: '10-K',
      totalAssets: 200,
      totalLiabilities: 100,
      totalEquity: 100,
    },
    {
      label: 'FY2023',
      periodEnd: '2023-12-31',
      filedDate: '2024-02-01',
      form: '10-K',
      totalAssets: 180,
      totalLiabilities: 90,
      totalEquity: 90,
    },
  ],
  ...overrides,
});

const makeBalanceSheetPanel = (
  symbol: string,
  overrides: Partial<BalanceSheetPanelData> = {},
): BalanceSheetPanelData => ({
  symbol,
  companyName: `${symbol} Inc.`,
  cik: `cik-${symbol}`,
  frequency: 'annual',
  latestFiling: {
    accessionNumber: `${symbol}-10k`,
    filingDate: '2025-02-01',
    form: '10-K',
  },
  sourceStatus: {
    companyfactsUsed: false,
  },
  availableConcepts: [],
  selectedCurrency: 'USD',
  availableCurrencies: ['EUR', 'USD'],
  mappingWarnings: [],
  availablePeriods: [],
  periods: [
    {
      label: 'FY2024',
      periodEnd: '2024-12-31',
      filedDate: '2025-02-01',
      form: '10-K',
      totalAssets: 200,
      totalLiabilities: 100,
      totalEquity: 100,
    },
    {
      label: 'FY2023',
      periodEnd: '2023-12-31',
      filedDate: '2024-02-01',
      form: '10-K',
      totalAssets: 180,
      totalLiabilities: 90,
      totalEquity: 90,
    },
  ],
  ...overrides,
});

const makeFinancialsWorkspace = (
  id: string,
  sourceCommand = '/fa AAPL annual',
  panel = makeFinancialsPanel('AAPL'),
): Workspace => ({
  id,
  name: id,
  createdAt: new Date('2026-04-13T10:00:00Z'),
  terminal: makeTerminalState([
    {
      id: `${id}-command`,
      type: 'command',
      content: sourceCommand,
      timestamp: new Date('2026-04-13T10:00:00Z'),
    },
    {
      id: `${id}-panel`,
      type: 'panel',
      content: { type: 'financials', data: panel },
      timestamp: new Date('2026-04-13T10:00:01Z'),
    },
  ]),
});

const makeEvent = (
  overrides: Partial<MappingsUpdatedEvent> = {},
): MappingsUpdatedEvent => ({
  changeKind: 'created',
  refreshScope: {
    fullRefresh: false,
    affectedScopes: [{ ticker: 'AAPL' }],
    affectedTargets: ['total_revenues'],
  },
  ...overrides,
});

describe('mappings refresh targeting', () => {
  it('extracts the latest refreshable earnings panel from workspace history', () => {
    const panel = getRefreshableEarningsPanel(makeWorkspace('workspace-aapl'));

    expect(panel).not.toBeNull();
    expect(panel?.sourceCommand).toBe('/em AAPL annual 2023 2024 --currency USD');
    expect(panel?.ticker).toBe('AAPL');
    expect(panel?.periodStartDate).toBe('2023-12-31');
    expect(panel?.periodEndDate).toBe('2024-12-31');
  });

  it('parses and preserves earnings currency flags in canonical refresh commands', () => {
    const panel = getRefreshableEarningsPanel(
      makeWorkspace(
        'workspace-earnings-currency',
        '/em AAPL annual 2023 2024 --currency EUR',
      ),
    );

    expect(panel?.currency).toBe('EUR');
    expect(panel?.sourceCommand).toBe('/em AAPL annual 2023 2024 --currency EUR');
  });

  it('falls back to panel selectedCurrency when the earnings source command omitted it', () => {
    const panel = getRefreshableEarningsPanel(
      makeWorkspace(
        'workspace-earnings-fallback',
        '/em AAPL annual 2023 2024',
        makeEarningsPanel('AAPL', {
          selectedCurrency: 'EUR',
          availableCurrencies: ['EUR', 'USD'],
        }),
      ),
    );

    expect(panel?.currency).toBe('EUR');
    expect(panel?.sourceCommand).toBe('/em AAPL annual 2023 2024 --currency EUR');
  });

  it('finds earnings panel even when a non-earnings panel appears after it', () => {
    const workspace: Workspace = {
      id: 'ws-mixed',
      name: 'ws-mixed',
      createdAt: new Date('2026-04-13T10:00:00Z'),
      terminal: makeTerminalState([
        {
          id: 'cmd-em',
          type: 'command',
          content: '/em AAPL annual 2023 2024',
          timestamp: new Date('2026-04-13T10:00:00Z'),
        },
        {
          id: 'panel-earnings',
          type: 'panel',
          content: { type: 'earnings', data: makeEarningsPanel('AAPL') },
          timestamp: new Date('2026-04-13T10:00:01Z'),
        },
        {
          id: 'cmd-company',
          type: 'command',
          content: '/AAPL',
          timestamp: new Date('2026-04-13T11:00:00Z'),
        },
        {
          id: 'panel-company',
          type: 'panel',
          content: {
            type: 'company',
            data: {
              symbol: 'AAPL',
              name: 'Apple Inc.',
              price: 200,
              change: 5,
              changePercent: 2.5,
              marketCap: 3000000000000,
            },
          },
          timestamp: new Date('2026-04-13T11:00:01Z'),
        },
      ]),
    };

    const panel = getRefreshableEarningsPanel(workspace);

    expect(panel).not.toBeNull();
    expect(panel?.sourceCommand).toBe('/em AAPL annual 2023 2024 --currency USD');
    expect(panel?.ticker).toBe('AAPL');
    expect(panel?.panelEntryId).toBe('panel-earnings');
  });

  it('returns null when workspace has only non-earnings panels', () => {
    const workspace: Workspace = {
      id: 'ws-no-earnings',
      name: 'ws-no-earnings',
      createdAt: new Date('2026-04-13T10:00:00Z'),
      terminal: makeTerminalState([
        {
          id: 'cmd-company',
          type: 'command',
          content: '/AAPL',
          timestamp: new Date('2026-04-13T10:00:00Z'),
        },
        {
          id: 'panel-company',
          type: 'panel',
          content: {
            type: 'company',
            data: {
              symbol: 'AAPL',
              name: 'Apple Inc.',
              price: 200,
              change: 5,
              changePercent: 2.5,
              marketCap: 3000000000000,
            },
          },
          timestamp: new Date('2026-04-13T10:00:01Z'),
        },
      ]),
    };

    const panel = getRefreshableEarningsPanel(workspace);

    expect(panel).toBeNull();
  });

  it('refreshes matching tickers and overlapping period scopes only', () => {
    const panel = getRefreshableEarningsPanel(makeWorkspace('workspace-aapl'));
    if (!panel) {
      throw new Error('Expected refreshable panel');
    }

    expect(
      shouldRefreshEarningsPanel(
        panel,
        makeEvent({
          refreshScope: {
            fullRefresh: false,
            affectedScopes: [
              {
                ticker: 'AAPL',
                periodStart: '2024-01-01',
                periodEnd: '2024-12-31',
              },
            ],
            affectedTargets: ['gross_profit'],
          },
        }),
      ),
    ).toBe(true);

    expect(
      shouldRefreshEarningsPanel(
        panel,
        makeEvent({
          refreshScope: {
            fullRefresh: false,
            affectedScopes: [
              {
                ticker: 'MSFT',
              },
            ],
            affectedTargets: ['gross_profit'],
          },
        }),
      ),
    ).toBe(false);

    expect(
      shouldRefreshEarningsPanel(
        panel,
        makeEvent({
          refreshScope: {
            fullRefresh: false,
            affectedScopes: [
              {
                ticker: 'AAPL',
                periodStart: '2021-01-01',
                periodEnd: '2021-12-31',
              },
            ],
            affectedTargets: ['gross_profit'],
          },
        }),
      ),
    ).toBe(false);
  });

  it('treats imported full refresh events as matching every earnings panel', () => {
    const panel = getRefreshableEarningsPanel(makeWorkspace('workspace-aapl'));
    if (!panel) {
      throw new Error('Expected refreshable panel');
    }

    expect(
      shouldRefreshEarningsPanel(
        panel,
        makeEvent({
          changeKind: 'imported',
          refreshScope: {
            fullRefresh: true,
            affectedScopes: [],
            affectedTargets: [],
          },
        }),
      ),
    ).toBe(true);
  });

  it('extracts the latest refreshable financials panel from workspace history', () => {
    const panel = getRefreshableFinancialsPanel(
      makeFinancialsWorkspace('workspace-financials'),
    );

    expect(panel).not.toBeNull();
    expect(panel?.sourceCommand).toBe('/fa AAPL annual');
    expect(panel?.ticker).toBe('AAPL');
    expect(panel?.periodStartDate).toBe('2023-12-31');
    expect(panel?.periodEndDate).toBe('2024-12-31');
  });

  it('extracts the latest refreshable balance sheet panel from workspace history', () => {
    const workspace: Workspace = {
      id: 'workspace-bs',
      name: 'workspace-bs',
      createdAt: new Date('2026-04-13T10:00:00Z'),
      terminal: makeTerminalState([
        {
          id: 'workspace-bs-command',
          type: 'command',
          content: '/bs AAPL annual',
          timestamp: new Date('2026-04-13T10:00:00Z'),
        },
        {
          id: 'workspace-bs-panel',
          type: 'panel',
          content: {
            type: 'balanceSheet',
            data: makeBalanceSheetPanel('AAPL'),
          },
          timestamp: new Date('2026-04-13T10:00:01Z'),
        },
      ]),
    };

    const panel = getRefreshableFinancialsPanel(workspace);

    expect(panel).not.toBeNull();
    expect(panel?.sourceCommand).toBe('/bs AAPL annual --currency USD');
    expect(panel?.ticker).toBe('AAPL');
  });

  it('parses balance sheet currency flags into canonical refresh commands', () => {
    const workspace: Workspace = {
      id: 'workspace-bs-currency',
      name: 'workspace-bs-currency',
      createdAt: new Date('2026-04-13T10:00:00Z'),
      terminal: makeTerminalState([
        {
          id: 'workspace-bs-currency-command',
          type: 'command',
          content: '/bs AAPL annual --currency EUR',
          timestamp: new Date('2026-04-13T10:00:00Z'),
        },
        {
          id: 'workspace-bs-currency-panel',
          type: 'panel',
          content: {
            type: 'balanceSheet',
            data: makeBalanceSheetPanel('AAPL'),
          },
          timestamp: new Date('2026-04-13T10:00:01Z'),
        },
      ]),
    };

    const panel = getRefreshableFinancialsPanel(workspace);

    expect(panel?.currency).toBe('EUR');
    expect(panel?.sourceCommand).toBe('/bs AAPL annual --currency EUR');
  });

  it('falls back to panel selectedCurrency when the balance sheet source command omitted it', () => {
    const workspace: Workspace = {
      id: 'workspace-bs-fallback',
      name: 'workspace-bs-fallback',
      createdAt: new Date('2026-04-13T10:00:00Z'),
      terminal: makeTerminalState([
        {
          id: 'workspace-bs-fallback-command',
          type: 'command',
          content: '/bs AAPL annual',
          timestamp: new Date('2026-04-13T10:00:00Z'),
        },
        {
          id: 'workspace-bs-fallback-panel',
          type: 'panel',
          content: {
            type: 'balanceSheet',
            data: makeBalanceSheetPanel('AAPL', {
              selectedCurrency: 'EUR',
              availableCurrencies: ['EUR', 'USD'],
            }),
          },
          timestamp: new Date('2026-04-13T10:00:01Z'),
        },
      ]),
    };

    const panel = getRefreshableFinancialsPanel(workspace);

    expect(panel?.currency).toBe('EUR');
    expect(panel?.sourceCommand).toBe('/bs AAPL annual --currency EUR');
  });

  it('refreshes financials panels using the same ticker and period overlap rules', () => {
    const panel = getRefreshableFinancialsPanel(
      makeFinancialsWorkspace('workspace-financials'),
    );
    if (!panel) {
      throw new Error('Expected refreshable financials panel');
    }

    expect(
      shouldRefreshFinancialsPanel(
        panel,
        makeEvent({
          refreshScope: {
            fullRefresh: false,
            affectedScopes: [{ ticker: 'AAPL', periodStart: '2024-01-01' }],
            affectedTargets: ['total_assets'],
          },
        }),
      ),
    ).toBe(true);

    expect(
      shouldRefreshFinancialsPanel(
        panel,
        makeEvent({
          refreshScope: {
            fullRefresh: false,
            affectedScopes: [{ ticker: 'MSFT' }],
            affectedTargets: ['total_assets'],
          },
        }),
      ),
    ).toBe(false);
  });
});

describe('refreshAffectedEarningsPanels', () => {
  it('replaces affected earnings panels in place after a successful refresh', async () => {
    const workspace = makeWorkspace('workspace-aapl');
    const execute = mock(
      async (): Promise<TerminalCommandResponse> => ({
        kind: 'panel',
        panel: {
          type: 'earnings',
          data: makeEarningsPanel('AAPL', {
            periods: [
              {
                label: 'FY2024',
                displayLabel: 'FY2024',
                periodEnd: '2024-12-31',
                filedDate: '2025-02-01',
                form: '10-K',
                totalRevenues: 123,
              },
            ],
          }),
        },
      }),
    );
    const replacePanel = mock(() => {});
    const reportFailure = mock(() => {});

    await refreshAffectedEarningsPanels({
      workspaces: [workspace],
      event: makeEvent(),
      execute,
      replacePanel,
      reportFailure,
      inFlightKeys: new Set<string>(),
    });

    expect(execute).toHaveBeenCalledTimes(1);
    expect(replacePanel).toHaveBeenCalledTimes(1);
    expect(reportFailure).not.toHaveBeenCalled();
  });

  it('reports failures when the refresh does not return an earnings panel', async () => {
    const workspace = makeWorkspace('workspace-aapl');
    const panel = getRefreshableEarningsPanel(workspace);
    if (!panel) {
      throw new Error('Expected refreshable panel');
    }
    const execute = mock(
      async (): Promise<TerminalCommandResponse> => ({
        kind: 'text',
        content: 'unexpected',
      }),
    );
    const replacePanel = mock(() => {});
    const reportFailure = mock(() => {});

    await refreshAffectedEarningsPanels({
      workspaces: [workspace],
      event: makeEvent(),
      execute,
      replacePanel,
      reportFailure,
      inFlightKeys: new Set<string>(),
    });

    expect(replacePanel).not.toHaveBeenCalled();
    expect(reportFailure).toHaveBeenCalledTimes(1);
    expect(reportFailure).toHaveBeenNthCalledWith(
      1,
      panel,
      'Failed to refresh earnings after mapping update.',
    );
  });

  it('deduplicates refreshes that are already in flight', async () => {
    const workspace = makeWorkspace('workspace-aapl');
    const panel = getRefreshableEarningsPanel(workspace);
    if (!panel) {
      throw new Error('Expected refreshable panel');
    }

    const execute = mock(async (): Promise<TerminalCommandResponse> => {
      throw new Error('should not run');
    });
    const replacePanel = mock(() => {});
    const reportFailure = mock(() => {});
    const inFlightKeys = new Set<string>([refreshPanelKey(panel)]);

    await refreshAffectedEarningsPanels({
      workspaces: [workspace],
      event: makeEvent(),
      execute,
      replacePanel,
      reportFailure,
      inFlightKeys,
    });

    expect(execute).not.toHaveBeenCalled();
    expect(replacePanel).not.toHaveBeenCalled();
    expect(reportFailure).not.toHaveBeenCalled();
  });
});

describe('refreshAffectedFinancialsPanels', () => {
  it('replaces affected financials panels in place after a successful refresh', async () => {
    const workspace = makeFinancialsWorkspace('workspace-financials');
    const execute = mock(
      async (): Promise<TerminalCommandResponse> => ({
        kind: 'panel',
        panel: {
          type: 'financials',
          data: makeFinancialsPanel('AAPL', {
            periods: [
              {
                label: 'FY2024',
                periodEnd: '2024-12-31',
                filedDate: '2025-02-01',
                form: '10-K',
                totalAssets: 300,
              },
            ],
          }),
        },
      }),
    );
    const replacePanel = mock(() => {});
    const reportFailure = mock(() => {});

    await refreshAffectedFinancialsPanels({
      workspaces: [workspace],
      event: makeEvent({
        refreshScope: {
          fullRefresh: false,
          affectedScopes: [{ ticker: 'AAPL' }],
          affectedTargets: ['total_assets'],
        },
      }),
      execute,
      replacePanel,
      reportFailure,
      inFlightKeys: new Set<string>(),
    });

    expect(execute).toHaveBeenCalledTimes(1);
    expect(replacePanel).toHaveBeenCalledTimes(1);
    expect(reportFailure).not.toHaveBeenCalled();
  });

  it('reports failures when the refresh does not return a financials panel', async () => {
    const workspace = makeFinancialsWorkspace('workspace-financials');
    const panel = getRefreshableFinancialsPanel(workspace);
    if (!panel) {
      throw new Error('Expected refreshable financials panel');
    }

    const execute = mock(
      async (): Promise<TerminalCommandResponse> => ({
        kind: 'text',
        content: 'unexpected',
      }),
    );
    const replacePanel = mock(() => {});
    const reportFailure = mock(() => {});

    await refreshAffectedFinancialsPanels({
      workspaces: [workspace],
      event: makeEvent({
        refreshScope: {
          fullRefresh: false,
          affectedScopes: [{ ticker: 'AAPL' }],
          affectedTargets: ['total_assets'],
        },
      }),
      execute,
      replacePanel,
      reportFailure,
      inFlightKeys: new Set<string>(),
    });

    expect(replacePanel).not.toHaveBeenCalled();
    expect(reportFailure).toHaveBeenNthCalledWith(
      1,
      panel,
      'Failed to refresh financials after mapping update.',
    );
  });
});
