/* @vitest-environment happy-dom */

/* eslint-disable vue/one-component-per-file */

import type { SystemNetworkApi } from '#/api/system/network';

import { flushPromises, mount } from '@vue/test-utils';
import { defineComponent, h } from 'vue';

import NetworkList, {
  getCurrentEndpoint,
  getKeeperDisabledReason,
  isDeleting,
} from '@test-source/apps/web-antdv-next/src/views/system/network/list';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import routes from '#/router/routes/modules/system';

type FakeEventSourceListener = (event: Event) => void;

class FakeEventSource {
  static instances: FakeEventSource[] = [];

  closed = false;
  readonly listeners = new Map<string, Set<FakeEventSourceListener>>();

  constructor(
    readonly url: string,
    readonly options?: EventSourceInit,
  ) {
    FakeEventSource.instances.push(this);
  }

  addEventListener(type: string, listener: FakeEventSourceListener) {
    const listeners = this.listeners.get(type) || new Set();
    listeners.add(listener);
    this.listeners.set(type, listeners);
  }

  close() {
    this.closed = true;
  }

  dispatch(type: string, data: Record<string, unknown>) {
    if (this.closed) return;
    const event = new MessageEvent(type, { data: JSON.stringify(data) });
    this.listeners.get(type)?.forEach((listener) => listener(event));
  }

  removeEventListener(type: string, listener: FakeEventSourceListener) {
    this.listeners.get(type)?.delete(listener);
  }
}

const mocks = vi.hoisted(() => ({
  accessCodes: new Set<string>([
    'System:Network:Ddns:List',
    'System:Network:PortForward:List',
  ]),
  api: {
    deleteMapping: vi.fn(),
    disableKeeper: vi.fn(),
    enableKeeper: vi.fn(),
    getAgentStatus: vi.fn(),
    getList: vi.fn(),
    probe: vi.fn(),
    retry: vi.fn(),
  },
  ddnsReload: vi.fn(),
  messageSuccess: vi.fn(),
  modalOpenCreate: vi.fn(),
  modalOpenEdit: vi.fn(),
  tableApi: {
    getRows: vi.fn(() => []),
    reload: vi.fn(),
  },
  tableOptions: undefined as any,
}));

vi.mock('@vben/access', () => ({
  useAccess: () => ({
    hasAccessByCodes: (codes: string[]) =>
      codes.every((code) => mocks.accessCodes.has(code)),
  }),
}));

vi.mock('@vben/common-ui', () => ({
  Page: defineComponent({
    name: 'MockPage',
    setup(_, { slots }) {
      return () => h('main', { 'data-testid': 'page-root' }, slots.default?.());
    },
  }),
}));

vi.mock('antdv-next', () => ({
  message: { success: mocks.messageSuccess },
  Space: defineComponent({
    name: 'MockSpace',
    setup(_, { slots }) {
      return () => h('span', slots.default?.());
    },
  }),
  Tag: defineComponent({
    name: 'MockTag',
    setup(_, { slots }) {
      return () => h('span', slots.default?.());
    },
  }),
  Tabs: defineComponent({
    name: 'MockTabs',
    props: {
      activeKey: { default: '', type: String },
      items: { default: () => [], type: Array },
    },
    emits: ['update:activeKey'],
    setup(props, { emit }) {
      return () =>
        h(
          'nav',
          { 'data-testid': 'network-tabs' },
          (props.items as Array<{ key: string; label: string }>).map((item) =>
            h(
              'button',
              {
                'data-tab': item.key,
                onClick: () => emit('update:activeKey', item.key),
              },
              item.label,
            ),
          ),
        );
    },
  }),
  Typography: {
    Text: defineComponent({
      name: 'MockText',
      setup(_, { slots }) {
        return () => h('span', slots.default?.());
      },
    }),
  },
}));

vi.mock('#/components/ktTable', () => ({
  KtTable: defineComponent({
    name: 'MockKtTable',
    setup(_, { slots }) {
      return () =>
        h('section', { 'data-testid': 'network-table' }, [
          slots.headerControls?.(),
          slots.bodyCell?.({
            column: { key: 'syncStatus' },
            record: { syncStatus: 'pending' },
          }),
          slots.bodyCell?.({
            column: { key: 'keeper' },
            record: {
              keeperDesiredEnabled: true,
              keeperStatus: 'active',
            },
          }),
        ]);
    },
  }),
  useKtTable: vi.fn((options) => {
    mocks.tableOptions = options;
    return [vi.fn(), mocks.tableApi];
  }),
}));

vi.mock(
  '@test-source/apps/web-antdv-next/src/views/system/network/components/NetworkPortForwardModal',
  () => ({
    default: defineComponent({
      name: 'MockNetworkPortForwardModal',
      setup(_, { expose }) {
        expose({
          openCreate: mocks.modalOpenCreate,
          openEdit: mocks.modalOpenEdit,
        });
        return () => h('div');
      },
    }),
  }),
);

vi.mock(
  '@test-source/apps/web-antdv-next/src/views/system/network/components/NetworkDdnsTable',
  () => ({
    default: defineComponent({
      name: 'MockNetworkDdnsTable',
      setup(_, { expose }) {
        expose({ reload: mocks.ddnsReload });
        return () => h('section', { 'data-testid': 'ddns-table' });
      },
    }),
  }),
);

vi.mock(
  '@test-source/apps/web-antdv-next/src/views/system/network/components/NetworkEndpointHistoryDrawer',
  () => ({
    default: defineComponent({
      name: 'MockNetworkEndpointHistoryDrawer',
      setup(_, { expose }) {
        expose({ open: vi.fn() });
        return () => h('div');
      },
    }),
  }),
);

vi.mock('#/api/system/network', () => ({
  deleteNetworkPortForward: mocks.api.deleteMapping,
  disableNetworkPortForwardKeeper: mocks.api.disableKeeper,
  enableNetworkPortForwardKeeper: mocks.api.enableKeeper,
  getNetworkAgentStatus: mocks.api.getAgentStatus,
  getNetworkManagementEventsUrl: vi.fn(
    () => '/api/system/network/events/stream',
  ),
  getNetworkPortForwardList: mocks.api.getList,
  probeNetworkPortForward: mocks.api.probe,
  retryNetworkPortForward: mocks.api.retry,
}));

vi.mock('#/locales', () => ({
  $t: (key: string) => key,
}));

function createRow(
  overrides: Partial<SystemNetworkApi.PortForward> = {},
): SystemNetworkApi.PortForward {
  return {
    currentObservedAt: null,
    currentPublicIpv4: null,
    currentPublicPort: null,
    currentValidUntil: null,
    desiredPresence: 'present',
    desiredRevision: '42',
    externalPort: 45_678,
    id: '90071992547409930',
    internalPort: 45_678,
    isDeleted: false,
    keeperDesiredEnabled: false,
    keeperStatus: 'disabled',
    lastObservedAt: null,
    lastObservedIpv4: null,
    lastObservedPort: null,
    name: 'Game UDP',
    protocol: 'udp',
    reportedRevision: '41',
    syncStatus: 'synced',
    targetIpv4: '192.168.31.224',
    updateTime: '2026-07-22T00:00:00.000Z',
    ...overrides,
  };
}

describe('system network persisted list', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.clearAllMocks();
    FakeEventSource.instances = [];
    vi.stubGlobal('EventSource', FakeEventSource);
    mocks.tableOptions = undefined;
    mocks.accessCodes = new Set([
      'System:Network:Ddns:List',
      'System:Network:PortForward:List',
    ]);
    mocks.ddnsReload.mockResolvedValue(undefined);
    mocks.tableApi.getRows.mockReturnValue([]);
    mocks.tableApi.reload.mockResolvedValue(undefined);
    mocks.api.getAgentStatus.mockResolvedValue({
      agentId: 'nas-main',
      appliedRevision: '41',
      desiredRevision: '42',
      online: false,
      publishedRevision: '42',
      targetIpv4: '192.168.31.224',
    });
    Object.defineProperty(navigator, 'clipboard', {
      configurable: true,
      value: { writeText: vi.fn().mockResolvedValue(undefined) },
    });
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    vi.useRealTimers();
  });

  it('renders one Page root and the standard system list route', async () => {
    const wrapper = mount(NetworkList);
    await flushPromises();
    const system = routes.find((route) => route.name === 'System');
    const network = system?.children?.find(
      (route) => route.name === 'SystemNetwork',
    );

    expect(wrapper.findAll('[data-testid="page-root"]')).toHaveLength(1);
    expect(wrapper.find('[data-testid="network-table"]').exists()).toBe(true);
    expect(wrapper.find('[data-testid="ddns-table"]').exists()).toBe(true);
    expect(
      wrapper
        .findAll('[data-testid="network-tabs"] button')
        .map((item) => item.text()),
    ).toEqual(['system.network.portForwardTab', 'system.network.ddnsTab']);
    expect(network).toMatchObject({ path: '/system/network' });
    expect(String(network?.component)).toContain('network/list');
    expect(mocks.tableOptions.columns.map((item: any) => item.key)).toEqual([
      'name',
      'protocol',
      'externalPort',
      'internalTarget',
      'syncStatus',
      'keeper',
      'publicEndpoint',
      'lastObserved',
      'summary',
      'updateTime',
    ]);
  });

  it('keeps the active tab panel in the full-height flex layout', async () => {
    const wrapper = mount(NetworkList);
    await flushPromises();

    const shell = wrapper.get('[data-testid="network-content-shell"]');
    const portForwardPanel = wrapper.get('[data-testid="port-forward-panel"]');
    const ddnsPanel = wrapper.get('[data-testid="ddns-panel"]');

    expect(shell.classes()).toEqual(
      expect.arrayContaining(['flex', 'h-full', 'min-h-0', 'flex-col']),
    );
    expect(portForwardPanel.classes()).toEqual(
      expect.arrayContaining(['min-h-0', 'flex-1']),
    );
    expect(portForwardPanel.classes()).not.toContain('hidden');
    expect(ddnsPanel.classes()).toContain('hidden');

    await wrapper
      .get('[data-testid="network-tabs"] [data-tab="ddns"]')
      .trigger('click');
    await flushPromises();

    expect(portForwardPanel.classes()).toContain('hidden');
    expect(ddnsPanel.classes()).toEqual(
      expect.arrayContaining(['min-h-0', 'flex-1']),
    );
    expect(ddnsPanel.classes()).not.toContain('hidden');
    expect(mocks.ddnsReload).toHaveBeenCalledOnce();
  });

  it('filters tabs by List permission and never requests a forbidden list', async () => {
    mocks.accessCodes = new Set(['System:Network:Ddns:List']);
    const wrapper = mount(NetworkList);
    await flushPromises();

    expect(
      wrapper
        .findAll('[data-testid="network-tabs"] button')
        .map((item) => item.text()),
    ).toEqual(['system.network.ddnsTab']);
    expect(mocks.tableApi.reload).not.toHaveBeenCalled();
    expect(mocks.api.getAgentStatus).not.toHaveBeenCalled();
    expect(mocks.ddnsReload).toHaveBeenCalledOnce();
    expect(FakeEventSource.instances).toHaveLength(1);
  });

  it('routes list labels and status values through the network locale namespace', () => {
    const wrapper = mount(NetworkList);

    expect(mocks.tableOptions.tableTitle).toBe(
      'system.network.portForwardTitle',
    );
    expect(mocks.tableOptions.buttons[0].label).toBe(
      'system.network.createAction',
    );
    expect(
      mocks.tableOptions.columns.map((column: any) => column.title),
    ).toEqual([
      'system.network.name',
      'system.network.protocol',
      'system.network.externalPort',
      'system.network.internalTarget',
      'system.network.syncStatus',
      'system.network.keeperState',
      'system.network.publicEndpoint',
      'system.network.lastObserved',
      'system.network.summary',
      'system.network.updateTime',
    ]);
    expect(
      mocks.tableOptions.rowActions.map((action: any) => action.label),
    ).toEqual([
      'system.network.editAction',
      'system.network.retryAction',
      'system.network.enableKeeperAction',
      'system.network.disableKeeperAction',
      'system.network.probeAction',
      'system.network.copyEndpointAction',
      'system.network.historyAction',
      'system.network.deleteAction',
    ]);
    expect(wrapper.text()).toContain('system.network.syncPending');
    expect(wrapper.text()).toContain('system.network.desiredOn');
    expect(wrapper.text()).toContain('system.network.keeperActive');
  });

  it('keeps unsupported Keeper actions visible-disabled with exact reasons', () => {
    mount(NetworkList);
    const actions = mocks.tableOptions.rowActions;
    const enable = actions.find((item: any) => item.key === 'keeper-enable');
    const probe = actions.find((item: any) => item.key === 'probe');
    const tcp = createRow({ protocol: 'tcp' });
    const splitPort = createRow({ internalPort: 32_000 });
    const disabledKeeper = createRow({ keeperDesiredEnabled: false });

    expect(enable.rowVisible(tcp)).toBe(true);
    expect(enable.disabled(tcp)).toBe(true);
    expect(enable.disabledReason(tcp)).toBe(
      'system.network.tcpKeeperUnsupported',
    );
    expect(probe.disabledReason(splitPort)).toBe(
      'system.network.udpSamePortRequired',
    );
    expect(probe.disabledReason(disabledKeeper)).toBe(
      'system.network.enableKeeperFirst',
    );
  });

  it('does not use Agent offline state to disable valid desired mutations', () => {
    mount(NetworkList);
    const actions = mocks.tableOptions.rowActions;
    const row = createRow({ keeperDesiredEnabled: false });
    const edit = actions.find((item: any) => item.key === 'edit');
    const enable = actions.find((item: any) => item.key === 'keeper-enable');

    expect(edit.disabled(row)).toBe(false);
    expect(enable.disabled(row)).toBe(false);
  });

  it('opens the shared modal from both create and edit table actions', async () => {
    mount(NetworkList);
    await flushPromises();
    const row = createRow();

    await mocks.tableOptions.buttons[0].onClick({});
    await mocks.tableOptions.rowActions
      .find((item: any) => item.key === 'edit')
      .onClick(row, {});

    expect(mocks.modalOpenCreate).toHaveBeenCalledWith('192.168.31.224');
    expect(mocks.modalOpenEdit).toHaveBeenCalledWith(row);
  });

  it('makes deleting rows immutable while preserving retry', () => {
    mount(NetworkList);
    const actions = mocks.tableOptions.rowActions;
    const row = createRow({
      desiredPresence: 'absent',
      syncStatus: 'deleting',
    });

    expect(isDeleting(row)).toBe(true);
    expect(actions.find((item: any) => item.key === 'edit').disabled(row)).toBe(
      true,
    );
    expect(
      actions.find((item: any) => item.key === 'delete').disabled(row),
    ).toBe(true);
    expect(
      actions.find((item: any) => item.key === 'retry').disabled(row),
    ).toBe(false);
  });

  it('copies only the exact API-approved current endpoint', async () => {
    mount(NetworkList);
    const action = mocks.tableOptions.rowActions.find(
      (item: any) => item.key === 'copy-endpoint',
    );
    const row = createRow({
      currentPublicIpv4: '123.45.67.89',
      currentPublicPort: 45_678,
    });

    expect(getCurrentEndpoint(row)).toBe('123.45.67.89:45678');
    expect(action.disabled(row)).toBe(false);
    await action.onClick(row);
    expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
      '123.45.67.89:45678',
    );
  });

  it('refreshes only for subscribed topic events and closes the stream on unmount', async () => {
    let finishReload: (() => void) | undefined;
    mocks.tableApi.reload.mockImplementation(
      () =>
        new Promise<void>((resolve) => {
          finishReload = resolve;
        }),
    );
    const wrapper = mount(NetworkList);
    await flushPromises();
    expect(mocks.tableApi.reload).toHaveBeenCalledTimes(1);
    expect(FakeEventSource.instances).toHaveLength(1);

    await vi.advanceTimersByTimeAsync(10_000);
    expect(mocks.tableApi.reload).toHaveBeenCalledTimes(1);
    finishReload?.();
    await flushPromises();
    expect(vi.getTimerCount()).toBe(0);

    FakeEventSource.instances[0]?.dispatch('heartbeat', {
      observedAt: '2026-07-23T00:00:00.000Z',
    });
    await flushPromises();
    expect(mocks.tableApi.reload).toHaveBeenCalledTimes(1);

    FakeEventSource.instances[0]?.dispatch('network-state-changed', {
      eventId: 'network-ddns-event-while-port-forward-active',
      observedAt: '2026-07-23T00:00:00.500Z',
      source: 'ddns',
    });
    await flushPromises();
    expect(mocks.tableApi.reload).toHaveBeenCalledTimes(1);
    expect(mocks.ddnsReload).not.toHaveBeenCalled();

    FakeEventSource.instances[0]?.dispatch('network-state-changed', {
      eventId: 'network-event-1',
      observedAt: '2026-07-23T00:00:01.000Z',
      source: 'reported',
    });
    await flushPromises();
    expect(mocks.tableApi.reload).toHaveBeenCalledTimes(2);

    finishReload?.();
    await flushPromises();
    wrapper.unmount();
    expect(FakeEventSource.instances[0]?.closed).toBe(true);
    FakeEventSource.instances[0]?.dispatch('network-state-changed', {
      eventId: 'network-event-2',
      observedAt: '2026-07-23T00:00:02.000Z',
      source: 'status',
    });
    await flushPromises();
    expect(mocks.tableApi.reload).toHaveBeenCalledTimes(2);
  });

  it('routes DDNS and replay-gap events to one active table without polling', async () => {
    const wrapper = mount(NetworkList);
    await flushPromises();
    expect(mocks.tableApi.reload).toHaveBeenCalledOnce();
    expect(mocks.ddnsReload).not.toHaveBeenCalled();
    expect(FakeEventSource.instances).toHaveLength(1);

    await wrapper
      .find('[data-testid="network-tabs"] [data-tab="ddns"]')
      .trigger('click');
    await flushPromises();
    expect(mocks.ddnsReload).toHaveBeenCalledOnce();

    FakeEventSource.instances[0]?.dispatch('network-state-changed', {
      eventId: 'network-reported-event-while-ddns-active',
      observedAt: '2026-07-23T00:00:00.500Z',
      source: 'reported',
    });
    await flushPromises();
    expect(mocks.ddnsReload).toHaveBeenCalledOnce();
    expect(mocks.tableApi.reload).toHaveBeenCalledOnce();

    FakeEventSource.instances[0]?.dispatch('network-state-changed', {
      eventId: 'network-ddns-event-1',
      observedAt: '2026-07-23T00:00:01.000Z',
      source: 'ddns',
    });
    await flushPromises();
    expect(mocks.ddnsReload).toHaveBeenCalledTimes(2);
    expect(mocks.tableApi.reload).toHaveBeenCalledOnce();

    FakeEventSource.instances[0]?.dispatch('network-state-changed', {
      eventId: 'network-ddns-event-1',
      observedAt: '2026-07-23T00:00:01.000Z',
      source: 'ddns',
    });
    FakeEventSource.instances[0]?.dispatch('heartbeat', {
      observedAt: '2026-07-23T00:00:02.000Z',
    });
    await flushPromises();
    expect(mocks.ddnsReload).toHaveBeenCalledTimes(2);

    FakeEventSource.instances[0]?.dispatch('snapshot-required', {
      observedAt: '2026-07-23T00:00:03.000Z',
    });
    await flushPromises();
    expect(mocks.ddnsReload).toHaveBeenCalledTimes(3);

    await vi.advanceTimersByTimeAsync(60_000);
    expect(mocks.ddnsReload).toHaveBeenCalledTimes(3);
    expect(vi.getTimerCount()).toBe(0);
  });
});

describe('network action constraints', () => {
  it('keeps Snowflake IDs and revisions as strings in row fixtures', () => {
    const row = createRow();
    expect(typeof row.id).toBe('string');
    expect(typeof row.desiredRevision).toBe('string');
    expect(getKeeperDisabledReason(row, false)).toBeUndefined();
  });
});
