import { act, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ProjectPublishAction } from '@/components/works/ProjectPublishAction';

const publishWorksProjectSourceMock = vi.hoisted(() => vi.fn());
const fetchCurrentWorksProjectStatusMock = vi.hoisted(() => vi.fn());

vi.mock('@/lib/works-square', () => ({
  publishWorksProjectSource: (...args: unknown[]) => publishWorksProjectSourceMock(...args),
  fetchCurrentWorksProjectStatus: (...args: unknown[]) => fetchCurrentWorksProjectStatusMock(...args),
}));

const project = {
  id: 'prj_space_cleaner',
  path: 'D:/repo/space-cleaner',
  name: 'space-cleaner',
  createdAt: '',
  updatedAt: '',
  lastOpenedAt: '',
};

const upload = {
  version_id: 'version-1',
  review_status: 'building',
  build_job_id: 'job-1',
  build_status: 'queued',
};

function projectStatus(buildStatus: string, buildErrorCode: string | null = null) {
  return {
    project: {
      app_id: 'space-cleaner',
      title: 'space-cleaner',
      summary: 'A project',
    },
    latest_version: {
      id: 'version-1',
      version_name: 'v1.0.0',
      review_status: buildStatus === 'succeeded' ? 'pending_review' : 'build_failed',
      change_log: '首次提交',
      build_job_id: 'job-1',
      build_status: buildStatus,
      build_error_code: buildErrorCode,
      build_error_message: 'Traceback: /srv/private/builder.py token=secret',
      release_id: buildStatus === 'succeeded' ? 'release-1' : null,
      created_at: '2026-08-07T12:00:00Z',
    },
    versions: [],
  };
}

async function flushSubmission() {
  await act(async () => {
    await Promise.resolve();
    await Promise.resolve();
  });
}

async function advancePoll() {
  await act(async () => {
    await vi.advanceTimersByTimeAsync(2_000);
  });
}

describe('ProjectPublishAction', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    publishWorksProjectSourceMock.mockReset();
    fetchCurrentWorksProjectStatusMock.mockReset();
    publishWorksProjectSourceMock.mockResolvedValue({ package: {}, upload });
  });

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

  it('submits automatic metadata and locks after Builder succeeds', async () => {
    fetchCurrentWorksProjectStatusMock.mockResolvedValue(projectStatus('succeeded'));
    render(<ProjectPublishAction project={project} projectType="mini_game" />);

    fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
    expect(screen.getByRole('button', { name: '正在检查并提交…' })).toBeDisabled();
    await flushSubmission();

    expect(publishWorksProjectSourceMock).toHaveBeenCalledWith({
      projectId: 'prj_space_cleaner',
      project: {
        app_id: 'makelore-prj-space-cleaner',
        title: 'space-cleaner',
        summary: 'space-cleaner，由 Makelore 创建的作品。',
        cover_url: null,
        category: 'mini_game',
        age_band: null,
        difficulty: null,
      },
    });
    expect(fetchCurrentWorksProjectStatusMock).not.toHaveBeenCalled();

    await advancePoll();

    expect(fetchCurrentWorksProjectStatusMock).toHaveBeenCalledWith(
      'makelore-prj-space-cleaner',
    );
    expect(screen.getByRole('button', { name: '已提交，等待运营审核' })).toBeDisabled();
    expect(screen.getByTestId('project-publish-status')).toHaveTextContent('已提交，等待运营审核');
  });

  it('shows a friendly Builder failure without raw details', async () => {
    fetchCurrentWorksProjectStatusMock.mockResolvedValue(
      projectStatus('failed', 'BROWSER_SMOKE_FAILED'),
    );
    render(<ProjectPublishAction project={project} projectType="mini_game" />);

    fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
    await flushSubmission();
    await advancePoll();

    const failure = screen.getByTestId('project-publish-failure');
    expect(failure).toHaveTextContent('自动打开作品时发现问题');
    expect(failure).toHaveTextContent('下一步：请让开发助手运行 npm run build');
    expect(failure).not.toHaveTextContent('BROWSER_SMOKE_FAILED');
    expect(failure).not.toHaveTextContent('Traceback');
    expect(failure).not.toHaveTextContent('/srv/private');
    expect(screen.getByRole('button', { name: '重新提交审核' })).toBeEnabled();
  });

  it('keeps an uncertain submission locked when status polling is interrupted', async () => {
    fetchCurrentWorksProjectStatusMock.mockRejectedValue(
      new Error('socket closed at /srv/private/status.py'),
    );
    render(<ProjectPublishAction project={project} projectType="mini_game" />);

    fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
    await flushSubmission();
    await advancePoll();

    const failure = screen.getByTestId('project-publish-failure');
    expect(failure).toHaveTextContent('暂时无法读取构建进度');
    expect(failure).toHaveTextContent('不需要重复提交');
    expect(failure).not.toHaveTextContent('socket closed');
    expect(screen.getByRole('button', { name: '已提交，请稍后查看' })).toBeDisabled();
  });

  it('shows a safe timeout instead of inviting a duplicate submission', async () => {
    const status = projectStatus('queued');
    status.latest_version.id = 'another-version';
    fetchCurrentWorksProjectStatusMock.mockResolvedValue(status);
    render(<ProjectPublishAction project={project} projectType="mini_game" />);

    fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
    await flushSubmission();
    await act(async () => {
      await vi.runAllTimersAsync();
    });

    const failure = screen.getByTestId('project-publish-failure');
    expect(failure).toHaveTextContent('构建时间比平时更长');
    expect(failure).toHaveTextContent('不需要重复提交');
    expect(screen.getByRole('button', { name: '已提交，请稍后查看' })).toBeDisabled();
    expect(fetchCurrentWorksProjectStatusMock).toHaveBeenCalledTimes(300);
  });

  it('sanitizes local and HTTP failures before rendering', async () => {
    publishWorksProjectSourceMock.mockRejectedValue(Object.assign(
      new Error('Traceback: failed at /srv/private/package.ts token=secret'),
      { code: 'INTERNAL_DATABASE_FAILURE', statusCode: 503 },
    ));
    render(<ProjectPublishAction project={project} projectType="mini_game" />);

    fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
    await flushSubmission();

    const failure = screen.getByTestId('project-publish-failure');
    expect(failure).toHaveTextContent('发布服务暂时不可用');
    expect(failure).not.toHaveTextContent('Traceback');
    expect(failure).not.toHaveTextContent('INTERNAL_DATABASE_FAILURE');
    expect(fetchCurrentWorksProjectStatusMock).not.toHaveBeenCalled();
  });

  it('turns a local package error into instructions for the user', async () => {
    publishWorksProjectSourceMock.mockRejectedValue(Object.assign(
      new Error('D:/repo/space-cleaner/package-lock.json does not exist'),
      { code: 'PROJECT_FILE_MISSING' },
    ));
    render(<ProjectPublishAction project={project} projectType="mini_game" />);

    fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
    await flushSubmission();

    const failure = screen.getByTestId('project-publish-failure');
    expect(failure).toHaveTextContent('项目文件不完整');
    expect(failure).toHaveTextContent('请让开发助手修复当前项目模板');
    expect(failure).not.toHaveTextContent('D:/repo');
    expect(failure).not.toHaveTextContent('PROJECT_FILE_MISSING');
    expect(fetchCurrentWorksProjectStatusMock).not.toHaveBeenCalled();
  });

  it('keeps generated metadata within the server limits', async () => {
    const longName = `作品-${'x'.repeat(250)}`;
    render(<ProjectPublishAction project={{ ...project, name: longName }} projectType="mini_program" />);

    fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
    await flushSubmission();

    const input = publishWorksProjectSourceMock.mock.calls[0]?.[0];
    expect(input.project.app_id).toBe('makelore-prj-space-cleaner');
    expect(input.project.title).toHaveLength(160);
    expect(input.project.summary).toBe(`${input.project.title}，由 Makelore 创建的作品。`);
    expect(input.project.category).toBe('mini_program');
  });
});
