import { beforeEach, describe, expect, it, vi } from 'vitest'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'

import { defaultApiClient } from './client'

function createJsonResponse(status: number, payload: unknown): Response {
  return new Response(JSON.stringify(payload), {
    status,
    headers: {
      'Content-Type': 'application/json',
    },
  })
}

describe('api client core', () => {
  beforeEach(() => {
    vi.restoreAllMocks()
  })

  it('injects bearer token via beforeRequest hook', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue(createJsonResponse(200, { ok: true })),
    )

    await defaultApiClient
      .createMethod<{ ok: boolean }>({
        apiBaseUrl: 'http://localhost:5577',
        pathname: '/auth/me',
        accessToken: 'demo-token',
        fallbackErrorMessage: '读取失败',
      })
      .send()

    expect(fetch).toHaveBeenCalledWith(
      'http://localhost:5577/api/auth/me',
      expect.objectContaining({
        headers: expect.objectContaining({
          Authorization: 'Bearer demo-token',
        }),
      }),
    )
  })

  it('returns null when 404 is allowed', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue(createJsonResponse(404, { message: 'Not Found' })),
    )

    const result = await defaultApiClient
      .createMethod<{ status: string } | null>({
        apiBaseUrl: 'http://localhost:5577',
        pathname: '/missing',
        fallbackErrorMessage: '读取失败',
        returnNullOnNotFound: true,
      })
      .send()

    expect(result).toBeNull()
  })

  it('does not duplicate api prefix when base url already ends with /api', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue(createJsonResponse(200, { ok: true })),
    )

    await defaultApiClient
      .createMethod<{ ok: boolean }>({
        apiBaseUrl: 'http://localhost:5577/api',
        pathname: '/auth/me',
        fallbackErrorMessage: '读取失败',
      })
      .send()

    expect(fetch).toHaveBeenCalledWith(
      'http://localhost:5577/api/auth/me',
      expect.any(Object),
    )
  })

  it('parses text responses when requested', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue(
        new Response('plain text body', {
          status: 200,
          headers: {
            'Content-Type': 'text/plain',
          },
        }),
      ),
    )

    const result = await defaultApiClient
      .createMethod<string>({
        apiBaseUrl: 'http://localhost:5577',
        pathname: '/text',
        fallbackErrorMessage: '读取失败',
        responseType: 'text',
      })
      .send()

    expect(result).toBe('plain text body')
  })

  it('unwraps standardized api envelope payload', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue(
        createJsonResponse(200, {
          code: 200,
          message: 'OK',
          data: {
            status: 'published',
          },
          timestamp: '2026-04-17T12:00:00.000Z',
          traceId: 'trace-envelope-success',
        }),
      ),
    )

    const result = await defaultApiClient
      .createMethod<{ status: string }>({
        apiBaseUrl: 'http://localhost:5577',
        pathname: '/resume/published',
        fallbackErrorMessage: '读取失败',
      })
      .send()

    expect(result).toEqual({ status: 'published' })
  })

  it('extracts error messages from json payloads', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue(createJsonResponse(500, { message: '服务繁忙' })),
    )

    await expect(
      defaultApiClient
        .createMethod({
          apiBaseUrl: 'http://localhost:5577',
          pathname: '/broken',
          fallbackErrorMessage: '读取失败',
        })
        .send(),
    ).rejects.toThrow('服务繁忙')
  })

  it('extracts standardized api envelope error with trace id', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue(
        createJsonResponse(500, {
          code: 500,
          message: 'Internal server error',
          data: null,
          timestamp: '2026-04-17T12:00:00.000Z',
          traceId: 'trace-envelope-error',
        }),
      ),
    )

    await expect(
      defaultApiClient
        .createMethod({
          apiBaseUrl: 'http://localhost:5577',
          pathname: '/broken',
          fallbackErrorMessage: '读取失败',
        })
        .send(),
    ).rejects.toThrow('Internal server error (traceId: trace-envelope-error)')
  })

  it('preserves transport errors from fetch', async () => {
    vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new TypeError('network down')))

    await expect(
      defaultApiClient
        .createMethod({
          apiBaseUrl: 'http://localhost:5577',
          pathname: '/broken',
          fallbackErrorMessage: '网络异常，请稍后重试',
        })
        .send(),
    ).rejects.toThrow('network down')
  })

  it('does not use direct Alova.request calls in domain facades', () => {
    const currentDir = fileURLToPath(new URL('.', import.meta.url))
    const domainFiles = ['auth.ts', 'resume.ts', 'ai.ts']
    const directCallPattern = /\bAlova\.request\s*\(/

    for (const fileName of domainFiles) {
      const fileContent = readFileSync(join(currentDir, fileName), 'utf-8')
      expect(directCallPattern.test(fileContent)).toBe(false)
    }
  })

  it('exposes method factories only and removes promise facades', () => {
    const currentDir = fileURLToPath(new URL('.', import.meta.url))
    const domainFiles = ['auth.ts', 'resume.ts', 'ai.ts']
    const legacyFacadeNames = [
      'loginWithPassword',
      'fetchCurrentUser',
      'postProtectedAction',
      'fetchPublishedResume',
      'fetchDraftResume',
      'fetchDraftResumeSummary',
      'updateDraftResume',
      'publishResume',
      'fetchPublishedResumeSummary',
      'fetchAiWorkbenchRuntime',
      'triggerAiWorkbenchAnalysis',
      'generateAiResumeOptimization',
      'applyAiResumeOptimization',
      'fetchCachedAiWorkbenchReports',
      'fetchCachedAiWorkbenchReport',
      'extractTextFromFile',
    ]

    for (const fileName of domainFiles) {
      const fileContent = readFileSync(join(currentDir, fileName), 'utf-8')
      expect(/\bexport\s+async\s+function\b/.test(fileContent)).toBe(false)

      for (const legacyFacadeName of legacyFacadeNames) {
        const legacyExportPattern = new RegExp(
          `\\bexport\\s+(?:async\\s+)?function\\s+${legacyFacadeName}\\b`,
        )
        expect(legacyExportPattern.test(fileContent)).toBe(false)
      }
    }
  })
})
