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

import { AppContext, type AppContextValue } from '../../src/frontend/app-context';
import { I18nProvider } from '../../src/frontend/i18n/react';
import { api } from '../../src/frontend/services/api';
import { mailhubTheme } from '../../src/frontend/theme';
import type {
  ApiToken,
  DnsCredential,
  RuntimeConfig,
  SmtpCredential,
  SmtpRelay,
  Webhook
} from '../../src/frontend/types';
import ApiTokens from '../../src/pages/ApiTokens';
import DnsApi from '../../src/pages/DnsApi';
import SmtpCredentials from '../../src/pages/SmtpCredentials';
import Webhooks from '../../src/pages/Webhooks';

describe('Integration resource resilience and deep links', () => {
  it('keeps an initial Webhook endpoint failure visible and never presents it as an empty list', async () => {
    const user = userEvent.setup();
    const endpoints = vi.spyOn(api, 'webhooks')
      .mockRejectedValueOnce(new Error('Webhook endpoint service unavailable'))
      .mockResolvedValue({ webhooks: [webhook] });
    vi.spyOn(api, 'webhookDeliveries').mockResolvedValue({ deliveries: [] });
    vi.spyOn(api, 'domains').mockResolvedValue({ domains: [] });
    vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes: [] });

    renderPage(<Webhooks />, '/integrations/webhooks', '/integrations/webhooks');

    const errorTitle = await screen.findByText('Webhook 端点加载失败');
    const alert = errorTitle.closest('.ant-alert');
    expect(alert).not.toBeNull();
    expect(within(alert as HTMLElement).getByText('Webhook endpoint service unavailable')).toBeTruthy();
    expect(screen.queryByText('暂无 Webhook。创建后即可接收投递状态回调。')).toBeNull();

    await user.click(within(alert as HTMLElement).getByRole('button', { name: /刷新/ }));
    expect(await screen.findByText(webhook.name)).toBeTruthy();
    expect(endpoints).toHaveBeenCalledTimes(2);
  });

  it('keeps an initial API token failure visible instead of presenting a business empty state', async () => {
    const user = userEvent.setup();
    const tokens = vi.spyOn(api, 'apiTokens')
      .mockRejectedValueOnce(new Error('API token service unavailable'))
      .mockResolvedValue({ tokens: [apiToken] });
    vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes: [] });
    renderPage(<ApiTokens />, '/integrations/api-keys', '/integrations/api-keys');

    const error = await screen.findByText('API token service unavailable');
    const alert = error.closest('.ant-alert');
    expect(alert).not.toBeNull();
    expect(document.querySelector('.empty-state')).toBeNull();
    expect(screen.queryByText('创建 API 密钥后可在列表和详情中复制完整 Token，并按最小权限限制邮箱访问范围。')).toBeNull();

    await user.click(within(alert as HTMLElement).getByRole('button', { name: /刷新/ }));
    expect(await screen.findByText(apiToken.name)).toBeTruthy();
    expect(tokens).toHaveBeenCalledTimes(2);
  });

  it('keeps an initial DNS credential failure visible instead of presenting a business empty state', async () => {
    const user = userEvent.setup();
    const credentials = vi.spyOn(api, 'dnsCredentials')
      .mockRejectedValueOnce(new Error('DNS credential service unavailable'))
      .mockResolvedValue({ credentials: [dnsCredential] });
    renderPage(<DnsApi />, '/integrations/dns', '/integrations/dns');

    const error = await screen.findByText('DNS credential service unavailable');
    const alert = error.closest('.ant-alert');
    expect(alert).not.toBeNull();
    expect(document.querySelector('.empty-state')).toBeNull();

    await user.click(within(alert as HTMLElement).getByRole('button', { name: /刷新/ }));
    expect(await screen.findByText(dnsCredential.name)).toBeTruthy();
    expect(credentials).toHaveBeenCalledTimes(2);

    await user.click(screen.getByRole('button', { name: /新增凭据/ }));
    expect(await findDrawer('新增 DNS 集成凭据')).toBeTruthy();
  });

  it('restores a DNS credential detail from the URL and uses the DNS-specific deletion warning', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'dnsCredentials').mockResolvedValue({ credentials: [dnsCredential] });
    const router = renderPage(
      <DnsApi />,
      '/integrations/dns',
      `/integrations/dns?credentialId=${dnsCredential.id}`
    );

    expect(await findDrawer(dnsCredential.name)).toBeTruthy();
    expect(new URLSearchParams(router.state.location.search).get('credentialId')).toBe(String(dnsCredential.id));

    await user.click(screen.getByRole('button', { name: `删除 ${dnsCredential.name}` }));
    expect(await screen.findByText('确认删除该 DNS 集成凭据？删除后将无法再通过此 Provider 管理对应 Zone。')).toBeTruthy();
    expect(screen.queryByText('确认删除该 API 密钥？')).toBeNull();
  });

  it('closes a list-opened DNS detail without leaving a duplicate list history entry', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'dnsCredentials').mockResolvedValue({ credentials: [dnsCredential] });
    const router = renderPage(
      <DnsApi />,
      '/integrations/dns',
      '/integrations/dns',
      appContext(runtimeConfig),
      { initialEntries: ['/overview', '/integrations/dns'], initialIndex: 1 }
    );

    await user.click(await screen.findByRole('row', { name: new RegExp(dnsCredential.name) }));
    await waitFor(() => expect(new URLSearchParams(router.state.location.search).get('credentialId')).toBe(String(dnsCredential.id)));
    const detail = await findDrawer(dnsCredential.name);
    await user.click(within(detail).getByRole('button', { name: 'Close' }));
    await waitFor(() => expect(new URLSearchParams(router.state.location.search).has('credentialId')).toBe(false));

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

  it('replaces an untouched DNS direct link when closing it', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'dnsCredentials').mockResolvedValue({ credentials: [dnsCredential] });
    const router = renderPage(
      <DnsApi />,
      '/integrations/dns',
      `/integrations/dns?credentialId=${dnsCredential.id}`,
      appContext(runtimeConfig),
      { initialEntries: ['/overview', `/integrations/dns?credentialId=${dnsCredential.id}`], initialIndex: 1 }
    );

    const detail = await findDrawer(dnsCredential.name);
    await user.click(within(detail).getByRole('button', { name: 'Close' }));
    await waitFor(() => expect(router.state.location.search).toBe(''));

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

  it('keeps relay navigation usable when the SMTP credential list fails', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'smtpCredential').mockResolvedValue({ credential: smtpCredential });
    vi.spyOn(api, 'smtpCredentials').mockRejectedValue(new Error('SMTP credential service unavailable'));
    vi.spyOn(api, 'smtpRelays').mockResolvedValue({ relays: [smtpRelay] });
    const router = renderPage(
      <SmtpCredentials />,
      '/integrations/smtp',
      `/integrations/smtp?tab=relays&relayId=${smtpRelay.id}`
    );

    const relayDetail = await findDrawer(smtpRelay.name);
    expect(within(relayDetail).getByText(`${smtpRelay.host}:${smtpRelay.port}`)).toBeTruthy();
    expect(new URLSearchParams(router.state.location.search).get('relayId')).toBe(String(smtpRelay.id));

    expect(screen.getAllByText(smtpRelay.name).length).toBeGreaterThan(0);
    await user.click(screen.getByRole('tab', { name: 'SMTP 凭据' }));
    expect(await screen.findByText('SMTP credential service unavailable')).toBeTruthy();
  });

  it('keeps credential navigation usable when the SMTP relay list fails', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'smtpCredential').mockResolvedValue({ credential: smtpCredential });
    vi.spyOn(api, 'smtpCredentials').mockResolvedValue({ credentials: [smtpCredential] });
    vi.spyOn(api, 'smtpRelays').mockRejectedValue(new Error('SMTP relay service unavailable'));
    const router = renderPage(
      <SmtpCredentials />,
      '/integrations/smtp',
      `/integrations/smtp?tab=credentials&credentialId=${smtpCredential.id}`
    );

    const credentialDetail = await findDrawer(smtpCredential.username);
    expect(within(credentialDetail).getAllByText(smtpCredential.username).length).toBeGreaterThan(0);
    expect(new URLSearchParams(router.state.location.search).get('credentialId')).toBe(String(smtpCredential.id));

    await user.click(screen.getByRole('tab', { name: '发送中继' }));
    expect(await screen.findByText('SMTP relay service unavailable')).toBeTruthy();
  });

  it('closes a list-opened SMTP relay without leaving the detail behind the list', async () => {
    const user = userEvent.setup();
    mockSmtpApis();
    const listPath = '/integrations/smtp?tab=relays';
    const router = renderPage(
      <SmtpCredentials />,
      '/integrations/smtp',
      listPath,
      appContext(runtimeConfig),
      { initialEntries: ['/overview', listPath], initialIndex: 1 }
    );

    await user.click(await screen.findByRole('row', { name: new RegExp(smtpRelay.name) }));
    await waitFor(() => expect(new URLSearchParams(router.state.location.search).get('relayId')).toBe(String(smtpRelay.id)));
    const detail = await findDrawer(smtpRelay.name);
    await user.click(within(detail).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('replaces an untouched SMTP credential direct link while preserving its tab', async () => {
    const user = userEvent.setup();
    mockSmtpApis();
    const detailPath = `/integrations/smtp?tab=credentials&credentialId=${smtpCredential.id}`;
    const router = renderPage(
      <SmtpCredentials />,
      '/integrations/smtp',
      detailPath,
      appContext(runtimeConfig),
      { initialEntries: ['/overview', detailPath], initialIndex: 1 }
    );

    const detail = await findDrawer(smtpCredential.username);
    await user.click(within(detail).getByRole('button', { name: 'Close' }));
    await waitFor(() => {
      const params = new URLSearchParams(router.state.location.search);
      expect(params.get('tab')).toBe('credentials');
      expect(params.has('credentialId')).toBe(false);
    });

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

  it('restores API token and messages guide deep links while upgrading a public HTTP base URL to HTTPS', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'apiTokens').mockResolvedValue({ tokens: [apiToken] });
    vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes: [] });
    const router = renderPage(
      <ApiTokens />,
      '/integrations/api-keys',
      `/integrations/api-keys?guide=messages&tokenId=${apiToken.id}`,
      appContext({ ...runtimeConfig, appBaseUrl: 'http://mail-send.ss5.xyz' })
    );

    expect(await findDrawer(apiToken.name)).toBeTruthy();
    const guide = await findDrawer('API 使用文档');
    expect(within(guide).getByText('需要 messages:read 权限，并受 Token 的邮箱访问范围及账号收信权限共同限制。列表支持分页与邮箱、文件夹、已读状态、关键词筛选。')).toBeTruthy();
    expect(within(guide).getAllByText('https://mail-send.ss5.xyz/api/inbound-messages').length).toBeGreaterThan(0);
    expect(guide.textContent).not.toContain('http://mail-send.ss5.xyz');

    const params = new URLSearchParams(router.state.location.search);
    expect(params.get('guide')).toBe('messages');
    expect(params.get('tokenId')).toBe(String(apiToken.id));

    await user.click(within(guide).getByRole('button', { name: 'Close' }));
    await waitFor(() => expect(new URLSearchParams(router.state.location.search).has('guide')).toBe(false));
    expect(new URLSearchParams(router.state.location.search).get('tokenId')).toBe(String(apiToken.id));
  });

  it('closes nested token and guide history without reopening either drawer on Back', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'apiTokens').mockResolvedValue({ tokens: [apiToken] });
    vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes: [] });
    const listPath = '/integrations/api-keys';
    const router = renderPage(
      <ApiTokens />,
      '/integrations/api-keys',
      listPath,
      appContext(runtimeConfig),
      { initialEntries: ['/overview', listPath], initialIndex: 1 }
    );

    await user.click(await screen.findByRole('row', { name: new RegExp(apiToken.name) }));
    const tokenDetail = await findDrawer(apiToken.name);
    await user.click(within(tokenDetail).getByRole('button', { name: /API 使用文档/ }));
    const guide = await findDrawer('API 使用文档');
    expect(new URLSearchParams(router.state.location.search).get('guide')).toBe('send');

    await user.click(within(guide).getByText('收信 API', { selector: '.ant-collapse-header-text' }));
    await waitFor(() => expect(new URLSearchParams(router.state.location.search).get('guide')).toBe('messages'));
    await act(async () => {
      await router.navigate(-1);
    });
    expect(new URLSearchParams(router.state.location.search).get('guide')).toBe('send');
    await act(async () => {
      await router.navigate(1);
    });
    expect(new URLSearchParams(router.state.location.search).get('guide')).toBe('messages');

    await user.click(within(guide).getByRole('button', { name: 'Close' }));
    await waitFor(() => {
      const params = new URLSearchParams(router.state.location.search);
      expect(params.has('guide')).toBe(false);
      expect(params.get('tokenId')).toBe(String(apiToken.id));
    });
    await user.click(within(tokenDetail).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('closes a changed direct-link API guide before replacing its direct token detail', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'apiTokens').mockResolvedValue({ tokens: [apiToken] });
    vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes: [] });
    const directPath = `/integrations/api-keys?guide=send&tokenId=${apiToken.id}`;
    const router = renderPage(
      <ApiTokens />,
      '/integrations/api-keys',
      directPath,
      appContext(runtimeConfig),
      { initialEntries: ['/overview', directPath], initialIndex: 1 }
    );

    const tokenDetail = await findDrawer(apiToken.name);
    const guide = await findDrawer('API 使用文档');
    await user.click(within(guide).getByText('收信 API', { selector: '.ant-collapse-header-text' }));
    await waitFor(() => expect(new URLSearchParams(router.state.location.search).get('guide')).toBe('messages'));

    await act(async () => {
      await router.navigate(-1);
    });
    expect(new URLSearchParams(router.state.location.search).get('guide')).toBe('send');
    await act(async () => {
      await router.navigate(1);
    });
    expect(new URLSearchParams(router.state.location.search).get('guide')).toBe('messages');

    await user.click(within(guide).getByRole('button', { name: 'Close' }));
    await waitFor(() => {
      const params = new URLSearchParams(router.state.location.search);
      expect(params.has('guide')).toBe(false);
      expect(params.get('tokenId')).toBe(String(apiToken.id));
    });
    await user.click(within(tokenDetail).getByRole('button', { name: 'Close' }));
    await waitFor(() => expect(router.state.location.search).toBe(''));

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

function renderPage(
  element: ReactElement,
  routePath: string,
  initialEntry: string,
  context: AppContextValue = appContext(runtimeConfig),
  history?: { initialEntries: string[]; initialIndex: number }
) {
  const router = createMemoryRouter([
    { path: routePath, element },
    { path: '*', element: <div>other</div> }
  ], history || { initialEntries: [initialEntry], initialIndex: 0 });
  render(
    <ConfigProvider theme={{ ...mailhubTheme, token: { ...mailhubTheme.token, motion: false } }}>
      <AntApp>
        <I18nProvider>
          <AppContext.Provider value={context}>
            <RouterProvider router={router} />
          </AppContext.Provider>
        </I18nProvider>
      </AntApp>
    </ConfigProvider>
  );
  return router;
}

async function findDrawer(title: string) {
  const titleElement = await screen.findByText(title, { selector: '.ant-drawer-title' });
  const drawer = titleElement.closest('[role="dialog"]');
  expect(drawer).not.toBeNull();
  return drawer as HTMLElement;
}

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

function mockSmtpApis() {
  vi.spyOn(api, 'smtpCredential').mockResolvedValue({ credential: smtpCredential });
  vi.spyOn(api, 'smtpCredentials').mockResolvedValue({ credentials: [smtpCredential] });
  vi.spyOn(api, 'smtpRelays').mockResolvedValue({ relays: [smtpRelay] });
}

const runtimeConfig: RuntimeConfig = {
  appBaseUrl: 'https://mail.example.test',
  mailHostname: 'mail.example.test',
  sendingIp: '192.0.2.10',
  defaultSpfMechanisms: '',
  dmarcPolicy: 'none',
  dmarcRua: '',
  registrationRequiresApproval: false,
  sendRequiresVerified: true,
  engagementTrackingEnabled: true,
  listUnsubscribeMailto: '',
  listUnsubscribeUrl: '',
  listUnsubscribePostEnabled: false,
  feedbackIdEnabled: false,
  reportAbuseTo: '',
  csaComplaintsTo: '',
  bounceAddress: '',
  bounceEnvelopeEnabled: false,
  submission: {
    enabled: true,
    host: 'mail.example.test',
    ports: [{ port: 587, protocol: 'STARTTLS' }],
    username: 'primary-smtp',
    passwordSet: true,
    inboundEnabled: true,
    tls: true,
    requireTlsForAuth: true
  }
};

const webhook: Webhook = {
  id: 7,
  userId: 1,
  domainId: null,
  mailboxId: null,
  name: 'Production events',
  url: 'https://hooks.example.test/mailhub',
  secretPrefix: 'whsec_prod',
  events: ['sent', 'failed'],
  enabled: true,
  createdAt: '2026-07-14T00:00:00.000Z',
  updatedAt: '2026-07-14T00:00:00.000Z'
};

const dnsCredential: DnsCredential = {
  id: 3,
  userId: 1,
  name: 'Primary Cloudflare',
  provider: 'cloudflare',
  zoneName: 'example.test',
  defaultTtl: 600,
  credentialSet: true,
  createdAt: '2026-07-14T00:00:00.000Z',
  updatedAt: '2026-07-14T00:00:00.000Z'
};

const smtpCredential: SmtpCredential = {
  id: 5,
  userId: 1,
  username: 'application-smtp',
  passwordSet: true,
  createdAt: '2026-07-14T00:00:00.000Z',
  updatedAt: '2026-07-14T00:00:00.000Z'
};

const smtpRelay: SmtpRelay = {
  id: 8,
  userId: 1,
  name: 'SES outbound',
  host: 'email-smtp.us-east-1.amazonaws.com',
  port: 587,
  secure: false,
  username: 'relay-user',
  passwordSet: true,
  helo: 'mail.example.test',
  isDefault: true,
  createdAt: '2026-07-14T00:00:00.000Z',
  updatedAt: '2026-07-14T00:00:00.000Z'
};

const apiToken: ApiToken = {
  id: 9,
  userId: 1,
  name: 'Message reader',
  tokenPrefix: 'mh_message',
  tokenRecoverable: false,
  scopes: ['messages:read'],
  mailboxAccess: 'owner',
  mailboxIds: [],
  status: 'active',
  createdAt: '2026-07-14T00:00:00.000Z'
};
