import { App as AntApp, ConfigProvider } from 'antd';
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { AppContext, type AppContextValue } from '../../src/frontend/app-context';
import { I18nProvider } from '../../src/frontend/i18n/react';
import { detailHistoryState } from '../../src/frontend/navigation-state';
import { api } from '../../src/frontend/services/api';
import { mailhubTheme } from '../../src/frontend/theme';
import type { Domain, InboundMailbox, InboundMessage, RuntimeConfig } from '../../src/frontend/types';
import Inbox from '../../src/pages/Inbox';

describe('Inbox detail return path', () => {
  afterEach(() => vi.restoreAllMocks());

  it('keeps the original list history when another message is selected from an open detail', async () => {
    const user = userEvent.setup();
    const first = messageFixture(9, 1, 'Sent', 'First message');
    const second = messageFixture(10, 1, 'Sent', 'Second message');
    mockInboxApis([mailboxFixture(1)], [first, second]);
    vi.spyOn(api, 'inboundMessage').mockImplementation(async (id) => ({ message: id === second.id ? second : first }));
    const listPath = '/inbox?mailboxId=1&folder=Sent&page=2';
    const router = createInboxRouter(['/overview', listPath], 1);
    renderRouter(router);

    await user.click(await screen.findByRole('button', { name: 'First message · sender@example.test' }));
    await waitFor(() => expect(router.state.location.pathname).toBe('/inbox/messages/9'));
    expect(detailHistoryState(router.state.location.state)).toEqual({ listPath, depth: 1, origin: 'list' });

    fireEvent.click(screen.getByRole('button', { name: 'Second message · sender@example.test' }));
    await waitFor(() => expect(router.state.location.pathname).toBe('/inbox/messages/10'));
    expect(detailHistoryState(router.state.location.state)).toEqual({ listPath, depth: 2, origin: 'list' });

    fireEvent.click(screen.getByRole('button', { name: 'Close' }));
    await waitFor(() => expect(`${router.state.location.pathname}${router.state.location.search}`).toBe(listPath));

    await act(async () => {
      await router.navigate(-1);
    });
    expect(router.state.location.pathname).toBe('/overview');
  });

  it('keeps direct detail tabs navigable and closes them without reopening on Back', async () => {
    const user = userEvent.setup();
    const deepLinked = messageFixture(20, 2, 'Archive', 'Archived message');
    const messages = vi.fn(async () => ({ messages: [deepLinked], total: 1, page: 1, pageSize: 25 }));
    mockInboxApis([mailboxFixture(1), mailboxFixture(2)], [deepLinked]);
    vi.spyOn(api, 'inboundMessages').mockImplementation(messages);
    vi.spyOn(api, 'inboundMessage').mockResolvedValue({ message: deepLinked });
    const router = createInboxRouter(['/overview', '/inbox/messages/20'], 1);
    renderRouter(router);

    expect(await screen.findByText('Archived message')).toBeTruthy();
    await waitFor(() => {
      const params = new URLSearchParams(router.state.location.search);
      expect(params.get('mailboxId')).toBe('2');
      expect(params.get('folder')).toBe('Archive');
    });
    await waitFor(() => expect(messages).toHaveBeenCalledWith(expect.objectContaining({ mailboxId: 2, folder: 'Archive' })));

    await user.click(screen.getByRole('tab', { name: 'HTML 预览' }));
    await waitFor(() => expect(new URLSearchParams(router.state.location.search).get('tab')).toBe('html'));
    expect(detailHistoryState(router.state.location.state)).toEqual({
      listPath: '/inbox?mailboxId=2&folder=Archive',
      depth: 1,
      origin: 'direct'
    });

    await act(async () => {
      await router.navigate(-1);
    });
    expect(router.state.location.pathname).toBe('/inbox/messages/20');
    expect(new URLSearchParams(router.state.location.search).has('tab')).toBe(false);
    expect(detailHistoryState(router.state.location.state)).toBeNull();

    await act(async () => {
      await router.navigate(1);
    });
    expect(new URLSearchParams(router.state.location.search).get('tab')).toBe('html');
    expect(detailHistoryState(router.state.location.state)?.origin).toBe('direct');

    fireEvent.click(screen.getByRole('button', { name: 'Close' }));
    await waitFor(() => expect(router.state.location.pathname).toBe('/inbox'));
    const params = new URLSearchParams(router.state.location.search);
    expect(params.get('mailboxId')).toBe('2');
    expect(params.get('folder')).toBe('Archive');

    await act(async () => {
      await router.navigate(-1);
    });
    expect(router.state.location.pathname).toBe('/overview');
    expect(router.state.location.pathname).not.toContain('/messages/');
  });

  it('uses a detail drawer below the 1280px product breakpoint', async () => {
    vi.spyOn(window, 'matchMedia').mockImplementation((query) => mediaQueryList(query === '(min-width: 992px)', query));
    const deepLinked = messageFixture(30, 1, 'INBOX', 'Compact detail');
    mockInboxApis([mailboxFixture(1)], [deepLinked]);
    vi.spyOn(api, 'inboundMessage').mockResolvedValue({ message: deepLinked });
    const router = createInboxRouter(['/inbox/messages/30'], 0);
    renderRouter(router);

    expect(await screen.findByRole('button', { name: 'Close' })).toBeTruthy();
  });

  it('renders HTML-only messages in a sandboxed preview while keeping the source tab available', async () => {
    const user = userEvent.setup();
    const htmlOnly = {
      ...messageFixture(31, 1, 'INBOX', 'HTML only'),
      textBody: '',
      htmlBody: '<html><body><h1>Rendered invoice</h1><script>window.parent.pwned = true</script></body></html>'
    };
    mockInboxApis([mailboxFixture(1)], [htmlOnly]);
    vi.spyOn(api, 'inboundMessage').mockResolvedValue({ message: htmlOnly });
    const router = createInboxRouter(['/inbox/messages/31?mailboxId=1&folder=INBOX'], 0);
    renderRouter(router);

    const preview = await screen.findByTitle('HTML 预览') as HTMLIFrameElement;
    expect(screen.getByRole('tab', { name: 'HTML 预览' }).getAttribute('aria-selected')).toBe('true');
    expect(preview.getAttribute('sandbox')).toBe('');
    expect(preview.getAttribute('referrerpolicy')).toBe('no-referrer');
    expect(preview.getAttribute('srcdoc')).toContain('<h1>Rendered invoice</h1>');
    expect(preview.getAttribute('srcdoc')).toContain("script-src 'none'");

    await user.click(screen.getByRole('tab', { name: 'HTML 源码' }));
    expect(screen.getByText(/Rendered invoice/)).toBeTruthy();
    await waitFor(() => expect(new URLSearchParams(router.state.location.search).get('tab')).toBe('source'));
  });
});

describe('Inbox request and mailbox behavior', () => {
  afterEach(() => vi.restoreAllMocks());

  it('keeps the search draft synchronized with browser history', async () => {
    mockInboxApis([mailboxFixture(1)], []);
    const router = createInboxRouter([
      '/inbox?mailboxId=1&folder=INBOX&q=first%20query',
      '/inbox?mailboxId=1&folder=INBOX&q=second%20query'
    ], 1);
    renderRouter(router);

    const searchInput = await screen.findByPlaceholderText('搜索发件人、主题或正文预览') as HTMLInputElement;
    expect(searchInput.value).toBe('second query');

    await act(async () => {
      await router.navigate(-1);
    });
    await waitFor(() => expect(searchInput.value).toBe('first query'));

    await act(async () => {
      await router.navigate(1);
    });
    await waitFor(() => expect(searchInput.value).toBe('second query'));
  });

  it('does not let an older message-list response replace the current mailbox', async () => {
    const first = messageFixture(41, 1, 'INBOX', 'Stale mailbox message');
    const second = messageFixture(42, 2, 'INBOX', 'Current mailbox message');
    let resolveFirst: ((value: { messages: InboundMessage[]; total: number; page: number; pageSize: number }) => void) | undefined;
    const firstResponse = new Promise<{ messages: InboundMessage[]; total: number; page: number; pageSize: number }>((resolve) => {
      resolveFirst = resolve;
    });
    mockInboxApis([mailboxFixture(1), mailboxFixture(2)], []);
    const list = vi.spyOn(api, 'inboundMessages').mockImplementation(async (filters) => {
      const mailboxId = typeof filters === 'number' ? filters : filters?.mailboxId;
      if (mailboxId === 1) return firstResponse;
      return { messages: [second], total: 1, page: 1, pageSize: 25 };
    });
    const router = createInboxRouter(['/inbox?mailboxId=1&folder=INBOX'], 0);
    renderRouter(router);

    await waitFor(() => expect(list).toHaveBeenCalledWith(expect.objectContaining({ mailboxId: 1 })));
    await act(async () => {
      await router.navigate('/inbox?mailboxId=2&folder=INBOX');
    });
    expect(await screen.findByText('Current mailbox message')).toBeTruthy();

    await act(async () => {
      resolveFirst?.({ messages: [first], total: 1, page: 1, pageSize: 25 });
      await Promise.resolve();
    });
    expect(screen.queryByText('Stale mailbox message')).toBeNull();
    expect(screen.getByText('Current mailbox message')).toBeTruthy();
  });

  it('keeps message content visible when marking it as read fails', async () => {
    const unread = { ...messageFixture(50, 1, 'INBOX', 'Unread detail'), read: false };
    mockInboxApis([mailboxFixture(1, { unreadCount: 1 })], [unread]);
    vi.spyOn(api, 'inboundMessage').mockResolvedValue({ message: unread });
    vi.spyOn(api, 'markInboundMessageRead').mockRejectedValue(new Error('mark read failed'));
    const router = createInboxRouter(['/inbox/messages/50?mailboxId=1&folder=INBOX'], 0);
    renderRouter(router);

    expect(await screen.findByText('Unread detail body')).toBeTruthy();
    expect(await screen.findByText('mark read failed')).toBeTruthy();
  });

  it('shows unavailable folder counts and retries instead of displaying fake zeroes', async () => {
    vi.spyOn(window, 'matchMedia').mockImplementation((query) => mediaQueryList(query === '(min-width: 992px)' || query === '(min-width: 768px)', query));
    mockInboxApis([mailboxFixture(1)], []);
    const folders = vi.spyOn(api, 'inboundFolders')
      .mockRejectedValueOnce(new Error('folder request failed'))
      .mockResolvedValue({ folders: [{ name: 'INBOX', specialUse: null, messageCount: 3, unreadCount: 2 }] });
    const router = createInboxRouter(['/inbox?mailboxId=1&folder=INBOX'], 0);
    renderRouter(router);

    const warning = await screen.findByText('文件夹计数暂不可用。');
    expect(warning).toBeTruthy();
    expect(screen.getAllByLabelText('计数不可用').length).toBeGreaterThan(0);
    const alert = warning.closest('.ant-alert');
    expect(alert).toBeTruthy();
    await userEvent.click(within(alert as HTMLElement).getByRole('button', { name: /刷\s*新/ }));
    await waitFor(() => expect(folders).toHaveBeenCalledTimes(2));
    await waitFor(() => expect(screen.queryByText('文件夹计数暂不可用。')).toBeNull());
  });

  it('selects the mailbox with the most recent activity when the URL has no mailbox', async () => {
    const older = mailboxFixture(1, { lastMessageAt: '2026-07-14T00:00:00.000Z' });
    const recent = mailboxFixture(2, { lastMessageAt: '2026-07-15T00:00:00.000Z' });
    mockInboxApis([older, recent], []);
    const list = vi.spyOn(api, 'inboundMessages');
    const router = createInboxRouter(['/inbox'], 0);
    renderRouter(router);

    await waitFor(() => expect(list).toHaveBeenCalledWith(expect.objectContaining({ mailboxId: 2 })));
    expect(new URLSearchParams(router.state.location.search).get('mailboxId')).toBe('2');
  });

  it('updates mailbox settings from the routing workspace edit drawer', async () => {
    const user = userEvent.setup();
    const mailbox = mailboxFixture(1);
    mockInboxApis([mailbox], [], [domainFixture(1)]);
    const update = vi.spyOn(api, 'updateInboundMailbox').mockResolvedValue({
      mailbox: { ...mailbox, displayName: 'Updated inbox', aliases: ['sales'] }
    });
    const router = createInboxRouter(['/inbox?workspace=routing'], 0);
    renderRouter(router);

    const mailboxTitle = await screen.findByText(mailbox.address);
    const card = mailboxTitle.closest('.ant-card');
    expect(card).toBeTruthy();
    await user.click(within(card as HTMLElement).getByRole('button', { name: /修改/ }));
    const displayName = await screen.findByLabelText('显示名称');
    await user.clear(displayName);
    await user.type(displayName, 'Updated inbox');
    await user.type(screen.getByLabelText('新密码'), 'new-password-123');
    await user.clear(screen.getByLabelText('别名'));
    await user.type(screen.getByLabelText('别名'), 'sales');
    await user.click(screen.getByRole('button', { name: /保\s*存/ }));

    await waitFor(() => expect(update).toHaveBeenCalledWith(1, expect.objectContaining({
      displayName: 'Updated inbox',
      password: 'new-password-123',
      aliases: 'sales',
      status: 'active'
    })));
  });
});

function mediaQueryList(matches: boolean, media: string): MediaQueryList {
  return {
    matches,
    media,
    onchange: null,
    addListener: () => undefined,
    removeListener: () => undefined,
    addEventListener: () => undefined,
    removeEventListener: () => undefined,
    dispatchEvent: () => false
  };
}

function createInboxRouter(initialEntries: string[], initialIndex: number) {
  return createMemoryRouter([
    { path: '/inbox', element: <Inbox /> },
    { path: '/inbox/messages/:messageId', element: <Inbox /> },
    { path: '*', element: <div>other</div> }
  ], { initialEntries, initialIndex });
}

function renderRouter(router: ReturnType<typeof createInboxRouter>) {
  return render(
    <ConfigProvider theme={{ ...mailhubTheme, token: { ...mailhubTheme.token, motion: false } }}>
      <AntApp>
        <I18nProvider>
          <AppContext.Provider value={appContext}>
            <RouterProvider router={router} />
          </AppContext.Provider>
        </I18nProvider>
      </AntApp>
    </ConfigProvider>
  );
}

function mockInboxApis(mailboxes: InboundMailbox[], messages: InboundMessage[], domains: Domain[] = []) {
  vi.spyOn(api, 'domains').mockResolvedValue({ domains });
  vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes });
  vi.spyOn(api, 'inboundFolders').mockImplementation(async (mailboxId) => ({
    folders: [
      { name: 'INBOX', specialUse: null, messageCount: 0, unreadCount: 0 },
      { name: 'Sent', specialUse: '\\Sent', messageCount: mailboxId === 1 ? messages.length : 0, unreadCount: 0 },
      { name: 'Archive', specialUse: '\\Archive', messageCount: mailboxId === 2 ? messages.length : 0, unreadCount: 0 }
    ]
  }));
  vi.spyOn(api, 'inboundMessages').mockResolvedValue({ messages, total: messages.length, page: 1, pageSize: 25 });
}

function domainFixture(id: number): Domain {
  return {
    id,
    userId: 1,
    dnsCredentialId: null,
    smtpRelayId: null,
    domain: `example-${id}.test`,
    selector: 'mail',
    verificationToken: 'verification',
    dkimPublic: 'public-key',
    senderHost: `mail.example-${id}.test`,
    sendingIp: '192.0.2.10',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: '',
    catchAllAddress: '',
    status: {},
    createdAt: '2026-07-14T00:00:00.000Z',
    updatedAt: '2026-07-14T00:00:00.000Z'
  };
}

function mailboxFixture(id: number, overrides: Partial<InboundMailbox> = {}): InboundMailbox {
  return {
    id,
    userId: 1,
    domainId: id,
    domain: `example-${id}.test`,
    address: `inbox-${id}@example-${id}.test`,
    localPart: `inbox-${id}`,
    displayName: `Inbox ${id}`,
    aliases: [],
    forwardTo: [],
    keepForwarded: true,
    quotaMb: 1024,
    passwordSet: true,
    passwordRecoverable: false,
    status: 'active',
    messageCount: 2,
    unreadCount: 0,
    createdAt: '2026-07-14T00:00:00.000Z',
    updatedAt: '2026-07-14T00:00:00.000Z',
    ...overrides
  };
}

function messageFixture(id: number, mailboxId: number, folder: string, subject: string): InboundMessage {
  return {
    id,
    mailboxId,
    userId: 1,
    domainId: mailboxId,
    domain: `example-${mailboxId}.test`,
    mailboxAddress: `inbox-${mailboxId}@example-${mailboxId}.test`,
    folder,
    sender: 'sender@example.test',
    recipients: [`inbox-${mailboxId}@example-${mailboxId}.test`],
    subject,
    messageId: `<message-${id}@example.test>`,
    preview: `${subject} preview`,
    read: true,
    receivedAt: '2026-07-14T00:00:00.000Z',
    createdAt: '2026-07-14T00:00:00.000Z',
    updatedAt: '2026-07-14T00:00:00.000Z',
    textBody: `${subject} body`,
    htmlBody: `<p>${subject}</p>`,
    rawMessage: `Subject: ${subject}`
  };
}

const runtimeConfig: RuntimeConfig = {
  appBaseUrl: 'https://mail.example.test',
  mailHostname: 'mail.example.test',
  sendingIp: '192.0.2.10',
  defaultSpfMechanisms: '',
  dmarcPolicy: 'none',
  dmarcRua: '',
  sendRequiresVerified: true,
  engagementTrackingEnabled: true,
  listUnsubscribeMailto: '',
  listUnsubscribeUrl: '',
  listUnsubscribePostEnabled: false,
  feedbackIdEnabled: false,
  reportAbuseTo: '',
  csaComplaintsTo: '',
  bounceAddress: '',
  bounceEnvelopeEnabled: false
};

const appContext: AppContextValue = {
  user: { id: 1, username: 'admin', email: 'admin@example.test', role: 'admin', status: 'active' },
  config: runtimeConfig,
  refreshBootstrap: vi.fn(async () => undefined),
  logout: vi.fn(async () => undefined)
};
