// @vitest-environment node

import type { Server as HttpServer, IncomingHttpHeaders } from 'node:http';
import type { ViteDevServer } from 'vite';

import { Buffer } from 'node:buffer';
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { createServer as createHttpServer, request } from 'node:http';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gzipSync } from 'node:zlib';

import vue from '@vitejs/plugin-vue';
import { createServer } from 'vite';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

import { createOnlyOfficeSdkProxy, onlyOfficeDevelopmentPlugin, resolveOnlyOfficeDevOptions } from '../../vite/onlyoffice-plugin';

const root = fileURLToPath(new URL('../..', import.meta.url));
const manifestPath = resolve(root, 'config.json');
const iconNames = ['icon.png', 'icon@2x.png'] as const;
const proxyErrorText = 'ONLYOFFICE SDK 代理失败\n';
const maxSdkResponseBytes = 8 * 1024 * 1024;

async function listen(server: HttpServer): Promise<number> {
  await new Promise<void>((resolveListen, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      server.off('error', reject);
      resolveListen();
    });
  });
  const address = server.address();
  if (!address || typeof address === 'string') throw new Error('HTTP 测试服务没有可用监听端口');
  return address.port;
}

async function close(server: HttpServer): Promise<void> {
  await new Promise<void>((resolveClose, reject) => server.close((error) => (error ? reject(error) : resolveClose())));
}

async function forceClose(server: HttpServer): Promise<void> {
  server.closeAllConnections();
  await close(server);
}

async function createProxyOrigin(docsUrl: string, timeoutMs = 5_000): Promise<{ close: () => Promise<void>; origin: string }> {
  const handler = createOnlyOfficeSdkProxy({ docsUrl, timeoutMs });
  const proxyServer = createHttpServer((incomingRequest, response) => {
    void handler(incomingRequest, response, () => {
      response.statusCode = 404;
      response.end('Not Found');
    });
  });
  const proxyPort = await listen(proxyServer);
  return {
    close: () => close(proxyServer),
    origin: `http://127.0.0.1:${proxyPort}`,
  };
}

function hashSourceManifest(): string {
  return createHash('sha256').update(readFileSync(manifestPath)).digest('hex');
}

function expectNoStore(response: Response): void {
  expect(response.headers.get('cache-control')).toBe('no-store, no-cache, must-revalidate');
  expect(response.headers.get('pragma')).toBe('no-cache');
  expect(response.headers.get('expires')).toBe('0');
}

describe('onlyoffice Vite 开发服务', () => {
  const sourceManifestHash = hashSourceManifest();
  let options = resolveOnlyOfficeDevOptions({}, new Date('2026-08-11T01:02:03Z'));
  let origin: string;
  let port: number;
  let server: ViteDevServer;
  let upstreamHeaders: IncomingHttpHeaders;
  let upstreamMethod: string | undefined;
  let upstreamStatus = 200;
  let upstreamUrl: string | undefined;
  let upstreamServer: HttpServer;

  beforeAll(async () => {
    upstreamServer = createHttpServer((incomingRequest, response) => {
      upstreamHeaders = incomingRequest.headers;
      upstreamMethod = incomingRequest.method;
      upstreamUrl = incomingRequest.url;
      response.statusCode = upstreamStatus;
      const file = incomingRequest.url?.endsWith('plugins.css') ? 'plugins.css' : 'plugins.js';
      response.setHeader('Content-Type', file === 'plugins.css' ? 'text/css; charset=utf-8' : 'application/javascript; charset=utf-8');
      response.setHeader('ETag', `"fixture-${file}"`);
      response.setHeader('Last-Modified', 'Mon, 10 Aug 2026 12:00:00 GMT');
      response.end(incomingRequest.method === 'HEAD' ? undefined : `fixture:${file}`);
    });
    const upstreamPort = await listen(upstreamServer);
    options = resolveOnlyOfficeDevOptions({ ONLYOFFICE_DOCS_URL: `http://127.0.0.1:${upstreamPort}` }, new Date('2026-08-11T01:02:03Z'));
    server = await createServer({
      configFile: false,
      logLevel: 'silent',
      plugins: [vue(), onlyOfficeDevelopmentPlugin(options)],
      root,
      server: {
        host: '127.0.0.1',
        port: 0,
      },
    });
    await server.listen();

    const address = server.httpServer?.address();
    if (!address || typeof address === 'string') throw new Error('Vite 测试服务没有可用监听端口');
    port = address.port;
    origin = `http://127.0.0.1:${port}`;
  });

  afterAll(async () => {
    await server?.close();
    await close(upstreamServer);
  });

  it('在精确开发路径返回动态 manifest 和 no-store 响应头', async () => {
    const response = await fetch(`${origin}${options.basePath}/config.json?v=${options.startupId}`);

    expect(response.status).toBe(200);
    expectNoStore(response);
    await expect(response.json()).resolves.toMatchObject({
      guid: 'asc.{2D6D6CB6-F6FC-45B7-8EC7-0CC1940F1A6F}',
      variations: [{ url: `index.html?v=${options.startupId}` }],
      version: '0.1.0',
    });
  });

  it('返回空翻译语言清单，避免 SDK 继续请求不存在的语言文件', async () => {
    const response = await fetch(`${origin}${options.basePath}/translations/langs.json`);

    expect(response.status).toBe(200);
    expectNoStore(response);
    await expect(response.json()).resolves.toEqual([]);
  });

  it.each(['POST', 'PUT'])('manifest 拒绝 %s 并声明允许的方法', async (method) => {
    const response = await fetch(`${origin}${options.basePath}/config.json?v=${options.startupId}`, { method });

    expect(response.status).toBe(405);
    expect(response.headers.get('allow')).toBe('GET, HEAD');
    expectNoStore(response);
  });

  it('将嵌套开发入口重写到 Vite HTML 并改写 SDK 资源', async () => {
    const response = await fetch(`${origin}${options.basePath}/index.html?v=${options.startupId}`);
    const html = await response.text();

    expect(response.status).toBe(200);
    expectNoStore(response);
    expect(html).toContain('href="/onlyoffice-sdk/v1/plugins.css"');
    expect(html).toContain('src="/onlyoffice-sdk/v1/plugins.js"');
    expect(html).not.toContain('../v1/plugins.');
  });

  it.each(iconNames)('在开发路径返回与源码一致的白名单图标 %s', async (iconName) => {
    const source = readFileSync(resolve(root, 'resources', iconName));
    const response = await fetch(`${origin}${options.basePath}/resources/${iconName}`);
    const content = Buffer.from(await response.arrayBuffer());

    expect(response.status).toBe(200);
    expectNoStore(response);
    expect(content.byteLength).toBeGreaterThan(0);
    expect(content).toEqual(source);
  });

  it.each(iconNames)('白名单图标 %s 支持 HEAD 且不返回 body', async (iconName) => {
    const response = await fetch(`${origin}${options.basePath}/resources/${iconName}`, { method: 'HEAD' });
    const content = await response.arrayBuffer();

    expect(response.status).toBe(200);
    expectNoStore(response);
    expect(content.byteLength).toBe(0);
  });

  it.each(['icon-v2.png', 'icon.png.bak', 'missing.png'])('拒绝非白名单图标资源 %s', async (iconName) => {
    const response = await fetch(`${origin}${options.basePath}/resources/${iconName}`);

    expect(response.status).toBe(404);
  });

  it.each(['/@vite/client', '/src/main.ts', '/src/styles.css'])('为开发资源 %s 设置 no-store 响应头', async (pathname) => {
    const response = await fetch(`${origin}${pathname}`);

    expect(response.status).toBe(200);
    expectNoStore(response);
  });

  it.each([
    ['plugins.js', 'application/javascript; charset=utf-8'],
    ['plugins.css', 'text/css; charset=utf-8'],
  ])('精确代理 SDK 资源 %s 并只转发白名单请求头', async (file, contentType) => {
    const response = await fetch(`${origin}/onlyoffice-sdk/v1/${file}?browser-secret=query-secret`, {
      headers: {
        accept: `${contentType}, */*`,
        authorization: 'Bearer authorization-secret',
        cookie: 'session=cookie-secret',
        'if-modified-since': 'Sun, 09 Aug 2026 12:00:00 GMT',
        'if-none-match': 'browser-etag',
        'x-browser-secret': 'private-header-value',
      },
    });

    expect(response.status).toBe(200);
    expect(await response.text()).toBe(`fixture:${file}`);
    expect(response.headers.get('content-type')).toBe(contentType);
    expect(response.headers.get('etag')).toBe(`"fixture-${file}"`);
    expect(response.headers.get('last-modified')).toBe('Mon, 10 Aug 2026 12:00:00 GMT');
    expectNoStore(response);
    expect(upstreamUrl).toBe(`/sdkjs-plugins/v1/${file}`);
    expect(upstreamHeaders.accept).toBe(`${contentType}, */*`);
    expect(upstreamHeaders['if-none-match']).toBe('browser-etag');
    expect(upstreamHeaders['if-modified-since']).toBe('Sun, 09 Aug 2026 12:00:00 GMT');
    expect(upstreamHeaders.authorization).toBeUndefined();
    expect(upstreamHeaders.cookie).toBeUndefined();
    expect(upstreamHeaders['x-browser-secret']).toBeUndefined();
  });

  it('使用 HEAD 请求 SDK 上游且不返回 body', async () => {
    const response = await fetch(`${origin}/onlyoffice-sdk/v1/plugins.js`, { method: 'HEAD' });
    const body = await response.arrayBuffer();

    expect(response.status).toBe(200);
    expect(body.byteLength).toBe(0);
    expect(upstreamMethod).toBe('HEAD');
    expectNoStore(response);
  });

  it('sdk 精确资源拒绝非 GET/HEAD 方法', async () => {
    const response = await fetch(`${origin}/onlyoffice-sdk/v1/plugins.js`, { method: 'POST' });

    expect(response.status).toBe(405);
    expect(response.headers.get('allow')).toBe('GET, HEAD');
    expectNoStore(response);
  });

  it('正式插件日志只记录固定失败类别', async () => {
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
    upstreamStatus = 500;

    try {
      const response = await fetch(`${origin}/onlyoffice-sdk/v1/plugins.js?secret=query`, {
        headers: { authorization: 'Bearer secret', cookie: 'private=cookie' },
      });

      expect(response.status).toBe(502);
      expect(consoleError.mock.calls).toEqual([['[onlyoffice-sdk-proxy] status']]);
      expect(JSON.stringify(consoleError.mock.calls)).not.toMatch(/127\.0\.0\.1|secret|private|fixture/u);
    } finally {
      upstreamStatus = 200;
      consoleError.mockRestore();
    }
  });

  it.each(['/onlyoffice-sdk/v1/unknown.js', '/onlyoffice-sdk/v1/plugins.js.map', '/onlyoffice-sdk/v2/plugins.js'])(
    '未知 SDK 路径返回 404：%s',
    async (pathname) => {
      const response = await fetch(`${origin}${pathname}`);

      expect(response.status).toBe(404);
      expectNoStore(response);
    },
  );

  it.each([
    '/onlyoffice-plugins/0.1.1-dev/eln-template-annotator/config.json',
    `${options.basePath}-similar/index.html`,
    `${options.basePath}/index.html-similar`,
  ])('拒绝未知 release 或相似前缀 %s', async (pathname) => {
    const response = await fetch(`${origin}${pathname}`);

    expect(response.status).toBe(404);
  });

  it('拒绝开发 release 路径中的编码路径穿越', async () => {
    const status = await new Promise<number | undefined>((resolveStatus, reject) => {
      const clientRequest = request(
        {
          host: '127.0.0.1',
          path: `${options.basePath}/%2e%2e/index.html`,
          port,
        },
        (response) => {
          response.resume();
          response.on('end', () => resolveStatus(response.statusCode));
        },
      );
      clientRequest.on('error', reject);
      clientRequest.end();
    });

    expect(status).toBe(404);
  });

  it.each([`${options.basePath}/resources/../index.html`, `${options.basePath}/resources/%2e%2e/index.html`, `${options.basePath}/resources%2ficon.png`])(
    '拒绝图标路径穿越或编码斜杠 %s',
    async (path) => {
      const status = await new Promise<number | undefined>((resolveStatus, reject) => {
        const clientRequest = request({ host: '127.0.0.1', path, port }, (response) => {
          response.resume();
          response.on('end', () => resolveStatus(response.statusCode));
        });
        clientRequest.on('error', reject);
        clientRequest.end();
      });

      expect(status).toBe(404);
    },
  );

  it('开发请求不写回正式 manifest', () => {
    expect(hashSourceManifest()).toBe(sourceManifestHash);
  });
});

describe('onlyoffice SDK 代理失败语义', () => {
  async function expectFixedProxyFailure(response: Response): Promise<void> {
    expect(response.status).toBe(502);
    expect(response.headers.get('content-type')).toBe('text/plain; charset=utf-8');
    expectNoStore(response);
    const body = await response.text();
    expect(body).toBe(proxyErrorText);
    expect(body).not.toMatch(/127\.0\.0\.1|secret|private|upstream/u);
  }

  it.each([404, 500])('上游 %s 统一返回固定 502，且不泄露上游响应', async (status) => {
    const upstream = createHttpServer((_request, response) => {
      response.statusCode = status;
      response.end('upstream-private-body');
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      const response = await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js?secret=query`, {
        headers: { authorization: 'Bearer secret', cookie: 'private=cookie' },
      });
      await expectFixedProxyFailure(response);
    } finally {
      await proxy.close();
      await close(upstream);
    }
  });

  it('非 2xx 响应会主动取消未结束的上游 body', async () => {
    let upstreamClosed = false;
    const upstream = createHttpServer((_request, response) => {
      response.statusCode = 500;
      response.on('close', () => {
        upstreamClosed = true;
      });
      response.write('upstream-private-body');
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`));
      await vi.waitFor(() => expect(upstreamClosed).toBe(true));
    } finally {
      await proxy.close();
      await forceClose(upstream);
    }
  });

  it('拒绝跨 origin 重定向且不向重定向目标泄露条件请求头', async () => {
    let redirectedRequestCount = 0;
    let redirectedHeaders: IncomingHttpHeaders = {};
    const redirected = createHttpServer((incomingRequest, response) => {
      redirectedRequestCount += 1;
      redirectedHeaders = incomingRequest.headers;
      response.end('redirect-private-body');
    });
    const redirectedPort = await listen(redirected);
    const upstream = createHttpServer((_request, response) => {
      response.statusCode = 302;
      response.setHeader('Location', `http://127.0.0.1:${redirectedPort}/private-target`);
      response.end();
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      const response = await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`, {
        headers: { 'if-modified-since': 'validator-secret', 'if-none-match': 'etag-secret' },
      });
      await expectFixedProxyFailure(response);
      expect(redirectedRequestCount).toBe(0);
      expect(redirectedHeaders['if-none-match']).toBeUndefined();
      expect(redirectedHeaders['if-modified-since']).toBeUndefined();
    } finally {
      await proxy.close();
      await close(upstream);
      await close(redirected);
    }
  });

  it.each(['GET', 'HEAD'])('透传上游 304 及 validator，%s 响应无 body', async (method) => {
    const upstream = createHttpServer((_request, response) => {
      response.statusCode = 304;
      response.setHeader('ETag', 'upstream-etag');
      response.setHeader('Last-Modified', 'Mon, 10 Aug 2026 12:00:00 GMT');
      response.end('must-not-be-forwarded');
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      const response = await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`, { method });
      const body = await response.arrayBuffer();

      expect(response.status).toBe(304);
      expect(response.headers.get('etag')).toBe('upstream-etag');
      expect(response.headers.get('last-modified')).toBe('Mon, 10 Aug 2026 12:00:00 GMT');
      expectNoStore(response);
      expect(body.byteLength).toBe(0);
    } finally {
      await proxy.close();
      await close(upstream);
    }
  });

  it('上游忽略 identity 并返回 gzip 时按解码 body 代理', async () => {
    const source = 'window.Asc = { plugin: true };';
    const compressed = gzipSync(source);
    let upstreamAcceptEncoding: string | undefined;
    const upstream = createHttpServer((incomingRequest, response) => {
      upstreamAcceptEncoding = incomingRequest.headers['accept-encoding'];
      response.setHeader('Content-Encoding', 'gzip');
      response.setHeader('Content-Length', String(compressed.byteLength));
      response.setHeader('Content-Type', 'application/javascript; charset=utf-8');
      response.end(compressed);
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      const response = await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`, {
        headers: { 'accept-encoding': 'browser-secret-encoding' },
      });

      expect(response.status).toBe(200);
      expect(await response.text()).toBe(source);
      expect(response.headers.get('content-encoding')).toBeNull();
      expect(upstreamAcceptEncoding).toBe('identity');
    } finally {
      await proxy.close();
      await close(upstream);
    }
  });

  it('gzip 解码 body 超过 8 MiB 时返回固定 502', async () => {
    const compressed = gzipSync(Buffer.alloc(maxSdkResponseBytes + 1, 1));
    const upstream = createHttpServer((_request, response) => {
      response.setHeader('Content-Encoding', 'gzip');
      response.setHeader('Content-Length', String(compressed.byteLength));
      response.end(compressed);
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`));
    } finally {
      await proxy.close();
      await close(upstream);
    }
  });

  it('content-length 超过 8 MiB 时在读取 body 前返回固定 502', async () => {
    let upstreamClosed = false;
    const upstream = createHttpServer((_request, response) => {
      response.setHeader('Content-Length', String(maxSdkResponseBytes + 1));
      response.on('close', () => {
        upstreamClosed = true;
      });
      response.write('not-forwarded');
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`));
      await vi.waitFor(() => expect(upstreamClosed).toBe(true));
    } finally {
      await proxy.close();
      await close(upstream);
    }
  });

  it('无 content-length 的响应流超过 8 MiB 时返回固定 502', async () => {
    const chunk = Buffer.alloc(1024 * 1024, 1);
    const upstream = createHttpServer((_request, response) => {
      for (let index = 0; index < 9; index += 1) response.write(chunk);
      response.end();
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.css`));
    } finally {
      await proxy.close();
      await close(upstream);
    }
  });

  it('上游声明长度后截断响应流时返回固定 502', async () => {
    const upstream = createHttpServer((_request, response) => {
      response.setHeader('Content-Length', '1024');
      response.end('truncated-private-body');
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`));
    } finally {
      await proxy.close();
      await close(upstream);
    }
  });

  it('上游响应流中途错误时返回固定 502', async () => {
    const upstream = createHttpServer((_request, response) => {
      response.write('stream-private-body');
      response.destroy();
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);

    try {
      await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.css`));
    } finally {
      await proxy.close();
      await close(upstream);
    }
  });

  it('客户端提前关闭连接时立即取消上游请求', async () => {
    let resolveUpstreamStarted = (): void => undefined;
    const upstreamStarted = new Promise<void>((resolveStarted) => {
      resolveUpstreamStarted = resolveStarted;
    });
    let upstreamClosed = false;
    const upstream = createHttpServer((_request, response) => {
      resolveUpstreamStarted();
      response.on('close', () => {
        upstreamClosed = true;
      });
      response.write('partial');
    });
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`, 5_000);

    try {
      const clientRequest = request(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`);
      clientRequest.on('error', () => undefined);
      clientRequest.end();
      await upstreamStarted;
      clientRequest.destroy();
      await vi.waitFor(() => expect(upstreamClosed).toBe(true));
    } finally {
      await forceClose(upstream);
      await proxy.close();
    }
  });

  it('上游连接拒绝统一返回固定 502', async () => {
    const unavailable = createHttpServer();
    const unavailablePort = await listen(unavailable);
    await close(unavailable);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${unavailablePort}`);

    try {
      await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.css?secret=query`));
    } finally {
      await proxy.close();
    }
  });

  it('上游超时统一返回固定 502', async () => {
    const upstream = createHttpServer(() => undefined);
    const upstreamPort = await listen(upstream);
    const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`, 20);

    try {
      await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js?secret=query`));
    } finally {
      await proxy.close();
      await close(upstream);
    }
  });
});
