import { afterEach, describe, expect, it, vi } from 'vitest';
import {
  cancelIdentityPairing,
  connectSecurityEventTransmitter,
  createResponse,
  deleteOIDCBrowserSession,
  GatewayApiError,
  gatewayErrorMessage,
  getAPITask,
  getCurrentUser,
  getOpsManagementSkillMetadata,
  loginLocalAccount,
  listApiKeyAssignableModels,
  OIDC_BROWSER_SESSION_CREDENTIAL,
  startIdentityPairing,
  retireIdentityPairingSecurityEventConflict,
  validateIdentityRevision,
} from './api';
import { applyTaskSubmissionMode, runTask } from './lib/run-task';

describe('local login transport', () => {
  afterEach(() => {
    vi.useRealTimers();
    vi.unstubAllGlobals();
  });

  it('aborts after ten seconds and returns a stable login timeout message', async () => {
    vi.useFakeTimers();
    const fetchMock = vi.fn((_url: string, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
      init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')));
    }));
    vi.stubGlobal('fetch', fetchMock);

    const login = loginLocalAccount({ account: 'timeout-test-account', password: 'timeout-test-password' });
    const rejection = expect(login).rejects.toMatchObject({
      message: '登录请求超时，请稍后重试',
    });
    await vi.advanceTimersByTimeAsync(10_000);
    await rejection;

    const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(init.signal).toBeInstanceOf(AbortSignal);
    expect(init.signal?.aborted).toBe(true);
  });
});

describe('Gateway provisioning errors', () => {
  const cases = [
    ['GATEWAY_USER_NOT_PROVISIONED', '该账号尚未开通 EasyAI Gateway'],
    ['GATEWAY_USER_DISABLED', '该 Gateway 账号已停用，请联系管理员'],
    ['GATEWAY_TENANT_UNAVAILABLE', 'Gateway 租户尚未就绪，请联系管理员'],
    ['GATEWAY_USER_PROVISIONING_FAILED', 'Gateway 账号初始化失败，请稍后重试'],
    ['OIDC_BROWSER_SESSION_DISABLED', 'Gateway 浏览器会话尚未启用'],
    ['OIDC_SESSION_INVALID', '统一认证会话无效，请重新登录'],
    ['OIDC_SESSION_EXPIRED', '统一认证会话已过期，请重新登录'],
    ['OIDC_SESSION_REFRESH_UNAVAILABLE', '认证中心暂时不可用，请稍后重试'],
    ['OIDC_SESSION_STORE_UNAVAILABLE', '登录会话存储暂时不可用，请稍后重试'],
    ['OIDC_SESSION_CSRF_REJECTED', '登录会话来源校验失败，请刷新后重试'],
    ['IDENTITY_PAIRING_IN_PROGRESS', '请先完成或放弃当前统一认证配对'],
    ['IDENTITY_PAIRING_NOT_CANCELLABLE', '当前统一认证配置不能放弃'],
    ['IDENTITY_PAIRING_CONFLICT_NOT_RESOLVABLE', '当前配对状态已变化，请刷新后重试'],
    ['IDENTITY_VERSION_CONFLICT', '统一认证状态已变化，请刷新后重试'],
  ] as const;

  for (const [code, expected] of cases) {
    it(`maps ${code} to a stable Chinese status`, () => {
      expect(gatewayErrorMessage({ code, message: 'internal server message', status: 503 })).toBe(expected);
      expect(new GatewayApiError({ code, message: 'internal server message', status: 503 }).message).toContain(expected);
      expect(new GatewayApiError({ code, message: 'internal server message', status: 503 }).message).not.toContain('internal server message');
    });
  }

  it('keeps the server message for unrelated errors', () => {
    expect(gatewayErrorMessage({ code: 'OTHER_ERROR', message: '模型不可用', status: 503 })).toBe('模型不可用');
  });
});

describe('identity configuration transport', () => {
	afterEach(() => {
		vi.unstubAllGlobals();
	});

	it('keeps the one-time onboarding code in the request body and enforces write headers', async () => {
		const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ id: 'pairing', version: 1 }), {
			status: 202,
			headers: { 'Content-Type': 'application/json' },
		}));
		vi.stubGlobal('fetch', fetchMock);
		vi.stubGlobal('crypto', { randomUUID: () => '00000000-0000-4000-8000-000000000000' });

		await startIdentityPairing('manager-token', {
			authCenterUrl: 'https://auth.example.com',
			onboardingCode: 'one-time-code-must-stay-in-body',
			publicBaseUrl: 'https://api.gateway.example.com',
			webBaseUrl: 'https://gateway.example.com',
			localTenantKey: 'default',
			legacyJwtEnabled: false,
		});

		const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
		expect(url).not.toContain('one-time-code-must-stay-in-body');
		expect(JSON.parse(String(init.body)).onboardingCode).toBe('one-time-code-must-stay-in-body');
		expect(new Headers(init.headers).get('If-Match')).toBe('W/"0"');
		expect(new Headers(init.headers).get('Idempotency-Key')).toContain('identity-pair-');
	});

	it('sends the revision version when validating a draft or active runtime', async () => {
		const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ id: 'revision', version: 8 }), {
			status: 200,
			headers: { 'Content-Type': 'application/json' },
		}));
		vi.stubGlobal('fetch', fetchMock);
		vi.stubGlobal('crypto', { randomUUID: () => '00000000-0000-4000-8000-000000000000' });

		await validateIdentityRevision('manager-token', 'revision', 7);

		const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
		expect(new Headers(init.headers).get('If-Match')).toBe('W/"7"');
		expect(init.credentials).toBe('include');
	});

	it('cancels a pairing with its own ETag and an idempotency key', async () => {
		const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({
			id: 'pairing', status: 'cancelled', cleanupStatus: 'pending', version: 8,
		}), {
			status: 202,
			headers: { 'Content-Type': 'application/json' },
		}));
		vi.stubGlobal('fetch', fetchMock);
		vi.stubGlobal('crypto', { randomUUID: () => '00000000-0000-4000-8000-000000000000' });

		await cancelIdentityPairing('manager-token', 'pairing', 7);

		const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
		expect(url).toContain('/api/admin/system/identity/pairings/pairing/cancel');
		expect(init.method).toBe('POST');
		expect(init.body).toBeUndefined();
		expect(new Headers(init.headers).get('If-Match')).toBe('W/"7"');
		expect(new Headers(init.headers).get('Idempotency-Key')).toContain('identity-cancel-');
		expect(init.credentials).toBe('include');
	});

	it('retires an SSF conflict through the pairing-scoped recovery endpoint', async () => {
		const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({
			id: 'pairing', status: 'credentials_saved', version: 7,
		}), {
			status: 202,
			headers: { 'Content-Type': 'application/json' },
		}));
		vi.stubGlobal('fetch', fetchMock);
		vi.stubGlobal('crypto', { randomUUID: () => '00000000-0000-4000-8000-000000000000' });

		await retireIdentityPairingSecurityEventConflict('manager-token', 'pairing', 7);

		const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
		expect(url).toContain('/api/admin/system/identity/pairings/pairing/retire-conflicting-security-event');
		expect(init.method).toBe('POST');
		expect(new Headers(init.headers).get('If-Match')).toBe('W/"7"');
		expect(new Headers(init.headers).get('Idempotency-Key')).toContain('identity-retire-ssf-conflict-');
	});
});

describe('security event connection transport', () => {
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('sends the one-time machine credential only in the connection request', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ connected: true }), {
      status: 202,
      headers: { 'Content-Type': 'application/json' },
    }));
    vi.stubGlobal('fetch', fetchMock);
    vi.stubGlobal('crypto', { randomUUID: () => '00000000-0000-4000-8000-000000000000' });

    await connectSecurityEventTransmitter('manager-token', 'https://auth.example/ssf', 'gateway-machine', 'one-time-machine-secret');

    const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(init.method).toBe('PUT');
    expect(JSON.parse(String(init.body))).toEqual({
      transmitter_issuer: 'https://auth.example/ssf',
      management_client_id: 'gateway-machine',
      management_client_secret: 'one-time-machine-secret',
    });
    expect(new Headers(init.headers).get('Idempotency-Key')).toContain('ssf-connect-');
    expect(new Headers(init.headers).has('If-Match')).toBe(false);
  });

  it('sends If-Match when retrying an existing failed connection', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ connected: true }), {
      status: 202,
      headers: { 'Content-Type': 'application/json' },
    }));
    vi.stubGlobal('fetch', fetchMock);
    vi.stubGlobal('crypto', { randomUUID: () => '00000000-0000-4000-8000-000000000000' });

    await connectSecurityEventTransmitter('manager-token', 'https://auth.example/ssf', 'gateway-machine', 'one-time-machine-secret', 7);

    const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(new Headers(init.headers).get('If-Match')).toBe('W/"7"');
  });
});

describe('OIDC browser session transport', () => {
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('uses the shared cookie without sending a synthetic bearer token', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ sub: 'oidc-user' }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    }));
    vi.stubGlobal('fetch', fetchMock);

    await getCurrentUser(OIDC_BROWSER_SESSION_CREDENTIAL);

    const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(init.credentials).toBe('include');
    expect(new Headers(init.headers).has('Authorization')).toBe(false);
  });

  it('deletes the shared cookie with a credentialed request', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
    vi.stubGlobal('fetch', fetchMock);

    await deleteOIDCBrowserSession();

    const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(init.method).toBe('DELETE');
    expect(init.credentials).toBe('include');
    expect(new Headers(init.headers).has('Authorization')).toBe(false);
  });
});

describe('API Key permission resources', () => {
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('loads the user-owned resource pool independently from playable models', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ items: [] }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    }));
    vi.stubGlobal('fetch', fetchMock);

    await listApiKeyAssignableModels('user-token');

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toContain('/api/v1/api-keys/assignable-models');
    expect(new Headers(init.headers).get('Authorization')).toBe('Bearer user-token');
  });
});

describe('Public Agent resources', () => {
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('loads operations skill metadata without authorization', async () => {
    const metadata = {
      name: 'ai-gateway-ops-management',
      version: '1.0.2',
      displayName: 'AI Gateway 运维管理',
      modules: ['model-runtime'],
      fileName: 'ai-gateway-ops-management-v1.0.2.zip',
      downloadPath: '/api/v1/public/skills/ai-gateway-ops-management/download',
      apiDocsJsonPath: '/api/v1/openapi.json',
      apiDocsYamlPath: '/api/v1/openapi.yaml',
    };
    const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify(metadata), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    }));
    vi.stubGlobal('fetch', fetchMock);

    await expect(getOpsManagementSkillMetadata()).resolves.toEqual(metadata);

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toContain('/api/v1/public/skills/ai-gateway-ops-management/metadata');
    expect(new Headers(init.headers).has('Authorization')).toBe(false);
  });
});

describe('API documentation runner transports', () => {
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('runs Responses against the public OpenAI-compatible endpoint', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ id: 'resp-test', object: 'response' }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    }));
    vi.stubGlobal('fetch', fetchMock);

    await createResponse('sk-test', { model: 'gpt-test', input: 'hello', store: true, stream: false });

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toContain('/v1/responses');
    expect(init.method).toBe('POST');
    expect(new Headers(init.headers).get('Authorization')).toBe('Bearer sk-test');
    expect(JSON.parse(String(init.body))).toMatchObject({ model: 'gpt-test', input: 'hello', store: true, stream: false });
  });

  it('retrieves a task from the documented API path', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ id: 'task-123', status: 'running' }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    }));
    vi.stubGlobal('fetch', fetchMock);

    await getAPITask('sk-test', 'task-123');

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toContain('/api/v1/tasks/task-123');
    expect(init.method).toBe('GET');
  });

  it('removes every simulation switch from a real submission while preserving edited parameters', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ id: 'chatcmpl-real' }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    }));
    vi.stubGlobal('fetch', fetchMock);

    await runTask(
      'sk-test',
      { kind: 'chat.completions', model: 'gpt-fallback', prompt: 'fallback' },
      {
        submissionMode: 'production',
        requestBody: {
          model: 'gpt-real',
          messages: [{ role: 'user', content: 'edited body' }],
          temperature: 0.25,
          runMode: 'simulation',
          run_mode: 'simulation',
          simulation: true,
          testMode: true,
          test_mode: true,
        },
      },
    );

    const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(JSON.parse(String(init.body))).toEqual({
      model: 'gpt-real',
      messages: [{ role: 'user', content: 'edited body' }],
      temperature: 0.25,
    });
  });

  it('uses canonical simulation parameters without removing a model-specific mode field', () => {
    expect(applyTaskSubmissionMode({ model: 'video-model', mode: 'pro', testMode: false }, 'simulation')).toEqual({
      model: 'video-model',
      mode: 'pro',
      runMode: 'simulation',
      simulation: true,
    });
  });
});
