/* oxlint-disable typescript/no-explicit-any */
import { fireEvent, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { AccessMode } from '@/models/access-control'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import { AppModeEnum } from '@/types/app'
import { basePath } from '@/utils/var'
import { AppPublisher } from '../index'

const render = (ui: React.ReactElement) =>
  renderWithConsoleQuery(ui, {
    systemFeatures: { webapp_auth: { enabled: true } },
  })

const mockOnPublish = vi.fn()
const mockOnToggle = vi.fn()
const mockSetAppDetail = vi.fn()
const mockTrackEvent = vi.fn()
const mockRefetch = vi.fn()
const mockUseGetUserCanAccessApp = vi.fn()
const mockOpenAsyncWindow = vi.fn()
const mockFetchInstalledAppList = vi.fn()
const mockFetchAppDetail = vi.fn()
const mockToastError = vi.fn()
const mockWindowOpen = vi.fn()
const mockInvalidateAppWorkflow = vi.fn()

const sectionProps = vi.hoisted(() => ({
  summary: null as null | Record<string, any>,
  access: null as null | Record<string, any>,
  actions: null as null | Record<string, any>,
}))
const hotkeyMocks = vi.hoisted(() => ({
  hotkeys: [] as string[],
  handlers: [] as Array<(event: { preventDefault: () => void }) => void>,
}))

let mockAppDetail: Record<string, any> | null = null
let mockWorkspacePermissionKeys: string[] = ['tool.manage']

vi.mock('@tanstack/react-hotkeys', () => ({
  useHotkey: (hotkey: string, handler: (event: { preventDefault: () => void }) => void) => {
    hotkeyMocks.hotkeys.push(hotkey)
    hotkeyMocks.handlers.push(handler)
  },
}))

vi.mock('@/app/components/app/store', () => ({
  useStore: (
    selector: (state: {
      appDetail: Record<string, any> | null
      setAppDetail: typeof mockSetAppDetail
    }) => unknown,
  ) =>
    selector({
      appDetail: mockAppDetail,
      setAppDetail: mockSetAppDetail,
    }),
}))

vi.mock('@/hooks/use-format-time-from-now', () => ({
  useFormatTimeFromNow: () => ({
    formatTimeFromNow: () => 'moments ago',
  }),
}))

vi.mock('@/hooks/use-async-window-open', () => ({
  useAsyncWindowOpen: () => mockOpenAsyncWindow,
}))

vi.mock('@/service/access-control/use-app-access-control', () => ({
  useGetUserCanAccessApp: (params: unknown) => {
    mockUseGetUserCanAccessApp(params)
    return {
      data: { result: true },
      isLoading: false,
      refetch: mockRefetch,
    }
  },
  useAppWhiteListSubjects: () => ({
    data: { groups: [], members: [] },
    isLoading: false,
  }),
}))

vi.mock('@/service/explore', () => ({
  fetchInstalledAppList: (...args: unknown[]) => mockFetchInstalledAppList(...args),
}))

const mockPublishToCreatorsPlatform = vi.fn()

vi.mock('@/service/apps', () => ({
  fetchAppDetail: (...args: unknown[]) => mockFetchAppDetail(...args),
  publishToCreatorsPlatform: (...args: unknown[]) => mockPublishToCreatorsPlatform(...args),
}))

vi.mock('@/service/use-workflow', () => ({
  useInvalidateAppWorkflow: () => mockInvalidateAppWorkflow,
}))

vi.mock('@/service/use-tools', () => ({
  useWorkflowToolDetailByAppID: () => ({
    data: undefined,
    isLoading: false,
  }),
  useInvalidateAllWorkflowTools: () => vi.fn(),
  useInvalidateWorkflowToolDetailByAppID: () => vi.fn(),
}))

vi.mock('@/context/workspace-state', async () => {
  const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
  return createWorkspaceStateModuleMock(() => ({
    isCurrentWorkspaceManager: true,
    workspacePermissionKeys: mockWorkspacePermissionKeys,
  }))
})
vi.mock('@/context/permission-state', async () => {
  const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
  return createPermissionStateModuleMock(() => ({
    isCurrentWorkspaceManager: true,
    workspacePermissionKeys: mockWorkspacePermissionKeys,
  }))
})

vi.mock('@langgenius/dify-ui/toast', () => ({
  toast: {
    error: (...args: unknown[]) => mockToastError(...args),
  },
}))

vi.mock('@/app/components/base/amplitude', () => ({
  trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
}))

vi.mock('@/app/components/app/overview/embedded', () => ({
  default: ({ isShow, onClose }: { isShow: boolean; onClose: () => void }) =>
    isShow ? (
      <div data-testid="embedded-modal">
        embedded modal
        <button onClick={onClose}>close-embedded-modal</button>
      </div>
    ) : null,
}))

vi.mock('../../app-access-control', () => {
  const MockAccessControl = ({
    onConfirm,
    onClose,
  }: {
    onConfirm: () => Promise<void>
    onClose: () => void
  }) => (
    <div data-testid="access-control">
      <button onClick={() => void onConfirm()}>confirm-access-control</button>
      <button onClick={onClose}>close-access-control</button>
    </div>
  )

  return {
    default: MockAccessControl,
    AccessControl: MockAccessControl,
  }
})

vi.mock('@/app/components/tools/workflow-tool', () => ({
  WorkflowToolDrawer: ({ onHide }: { onHide: () => void }) => (
    <div data-testid="workflow-tool-drawer">
      workflow tool drawer
      <button onClick={onHide}>close-workflow-tool-drawer</button>
    </div>
  ),
}))

vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))

vi.mock('../sections', () => ({
  PublisherSummarySection: (props: Record<string, any>) => {
    sectionProps.summary = props
    return (
      <div>
        <button onClick={() => void props.handlePublish()}>publisher-summary-publish</button>
        <button onClick={() => void props.handleRestore()}>publisher-summary-restore</button>
      </div>
    )
  },
  PublisherAccessSection: (props: Record<string, any>) => {
    sectionProps.access = props
    return <button onClick={props.onClick}>publisher-access-control</button>
  },
  PublisherActionsSection: (props: Record<string, any>) => {
    sectionProps.actions = props
    return (
      <div>
        <button onClick={props.handleEmbed}>publisher-embed</button>
        <button onClick={() => void props.handleOpenInExplore()}>publisher-open-in-explore</button>
        {props.handleOpenRunConfig && (
          <>
            <button onClick={() => props.handleOpenRunConfig(props.appURL)}>
              publisher-run-config
            </button>
            <button onClick={() => props.handleOpenRunConfig(`${props.appURL}?mode=batch`)}>
              publisher-batch-run-config
            </button>
          </>
        )}
        <button onClick={props.onConfigureWorkflowTool}>publisher-workflow-tool</button>
      </div>
    )
  },
}))

describe('AppPublisher', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    hotkeyMocks.hotkeys.length = 0
    hotkeyMocks.handlers.length = 0
    sectionProps.summary = null
    sectionProps.access = null
    sectionProps.actions = null
    mockWorkspacePermissionKeys = ['tool.manage']
    mockAppDetail = {
      id: 'app-1',
      name: 'Demo App',
      mode: AppModeEnum.CHAT,
      access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
      site: {
        app_base_url: 'https://example.com',
        access_token: 'token-1',
      },
    }
    mockFetchInstalledAppList.mockResolvedValue({
      installed_apps: [{ id: 'installed-1' }],
    })
    mockFetchAppDetail.mockResolvedValue({
      id: 'app-1',
      access_mode: AccessMode.PUBLIC,
    })
    mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise<string>) => {
      return resolver()
    })
    Object.defineProperty(window, 'open', {
      writable: true,
      value: mockWindowOpen,
    })
  })

  it('should enable access permission query when the publish popover opens', async () => {
    render(<AppPublisher publishedAt={Date.now()} onToggle={mockOnToggle} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))

    expect(screen.getByText('publisher-summary-publish'))!.toBeInTheDocument()
    expect(mockOnToggle).toHaveBeenCalledWith(true)

    await waitFor(() => {
      expect(mockUseGetUserCanAccessApp).toHaveBeenCalledWith({
        appId: 'app-1',
        enabled: true,
      })
    })
    expect(mockRefetch).not.toHaveBeenCalled()
  })

  it('should publish and track the publish event', async () => {
    mockOnPublish.mockResolvedValue(undefined)

    render(<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-summary-publish'))

    await waitFor(() => {
      expect(mockOnPublish).toHaveBeenCalledTimes(1)
      expect(mockTrackEvent).toHaveBeenCalledWith(
        'app_published_time',
        expect.objectContaining({
          action_mode: 'app',
          app_id: 'app-1',
          app_name: 'Demo App',
        }),
      )
    })
  })

  it('should open the embedded modal from the actions section', () => {
    render(<AppPublisher publishedAt={Date.now()} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-embed'))

    expect(screen.getByTestId('embedded-modal'))!.toBeInTheDocument()
  })

  it('should collect hidden inputs before opening published run links from config actions', async () => {
    render(
      <AppPublisher
        publishedAt={Date.now()}
        inputs={[
          {
            variable: 'secret',
            label: 'Secret',
            type: 'text-input',
            required: true,
            hide: true,
            default: '',
          } as any,
        ]}
      />,
    )

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-run-config'))

    expect(
      screen.getByText(/(?:^|\.)overview\.appInfo\.workflowLaunchHiddenInputs\.title(?=$|:)/),
    ).toBeInTheDocument()

    fireEvent.change(screen.getByLabelText('Secret'), {
      target: { value: 'top-secret' },
    })
    fireEvent.click(
      screen.getByRole('button', { name: /(?:^|\.)overview\.appInfo\.launch(?=$|:)/ }),
    )

    await waitFor(() => {
      expect(mockWindowOpen).toHaveBeenCalledWith(
        `https://example.com${basePath}/chat/token-1?secret=${encodeURIComponent('top-secret')}`,
        '_blank',
      )
    })
  })

  it('should open batch run config links with the configured hidden inputs', async () => {
    mockAppDetail = {
      ...mockAppDetail,
      mode: AppModeEnum.WORKFLOW,
    }

    render(
      <AppPublisher
        publishedAt={Date.now()}
        inputs={[
          {
            variable: 'batch_secret',
            label: 'Batch Secret',
            type: 'text-input',
            required: true,
            hide: true,
            default: '',
          } as any,
        ]}
      />,
    )

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-batch-run-config'))

    fireEvent.change(screen.getByLabelText('Batch Secret'), {
      target: { value: 'batch-value' },
    })
    fireEvent.click(
      screen.getByRole('button', { name: /(?:^|\.)overview\.appInfo\.launch(?=$|:)/ }),
    )

    await waitFor(() => {
      expect(mockWindowOpen).toHaveBeenCalledWith(
        `https://example.com${basePath}/workflow/token-1?mode=batch&batch_secret=${encodeURIComponent('batch-value')}`,
        '_blank',
      )
    })
  })

  it('should keep workflow tool drawer mounted after closing the publish popover', () => {
    mockAppDetail = {
      ...mockAppDetail,
      mode: AppModeEnum.WORKFLOW,
    }

    render(<AppPublisher publishedAt={Date.now()} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-workflow-tool'))

    expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
    expect(screen.getByTestId('workflow-tool-drawer')).toBeInTheDocument()
  })

  it('should not open workflow tool drawer without tool.manage', () => {
    mockWorkspacePermissionKeys = []
    mockAppDetail = {
      ...mockAppDetail,
      mode: AppModeEnum.WORKFLOW,
    }

    render(<AppPublisher publishedAt={Date.now()} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-workflow-tool'))

    expect(screen.queryByTestId('workflow-tool-drawer')).not.toBeInTheDocument()
    expect(sectionProps.actions?.workflowToolAvailable).toBe(false)
  })

  it('should close embedded and access control panels through child callbacks', async () => {
    render(<AppPublisher publishedAt={Date.now()} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-embed'))
    fireEvent.click(screen.getByText('close-embedded-modal'))
    expect(screen.queryByTestId('embedded-modal')).not.toBeInTheDocument()

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-access-control'))
    expect(screen.getByTestId('access-control'))!.toBeInTheDocument()
    fireEvent.click(screen.getByText('close-access-control'))
    expect(screen.queryByTestId('access-control')).not.toBeInTheDocument()
  })

  it('should refresh app detail after access control confirmation', async () => {
    const { queryClient } = render(<AppPublisher publishedAt={Date.now()} />)
    const setQueryDataSpy = vi.spyOn(queryClient, 'setQueryData')

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-access-control'))

    expect(screen.getByTestId('access-control'))!.toBeInTheDocument()

    fireEvent.click(screen.getByText('confirm-access-control'))

    await waitFor(() => {
      expect(mockFetchAppDetail).toHaveBeenCalledWith({ url: '/apps', id: 'app-1' })
    })
    expect(setQueryDataSpy).toHaveBeenCalledWith(
      ['apps', 'detail', 'app-1'],
      expect.objectContaining({
        access_mode: AccessMode.PUBLIC,
      }),
    )
    expect(mockSetAppDetail).toHaveBeenCalledWith(
      expect.objectContaining({
        access_mode: AccessMode.PUBLIC,
      }),
    )
  })

  it('should open the installed explore page through the async window helper', async () => {
    let openedUrl = ''
    mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise<string>) => {
      openedUrl = await resolver()
    })

    render(<AppPublisher publishedAt={Date.now()} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-open-in-explore'))

    await waitFor(() => {
      expect(mockOpenAsyncWindow).toHaveBeenCalledTimes(1)
      expect(mockFetchInstalledAppList).toHaveBeenCalledWith('app-1')
      expect(openedUrl).toBe('/installed/installed-1')
      expect(sectionProps.actions?.appURL).toBe(`https://example.com${basePath}/chat/token-1`)
    })
  })

  it('should ignore the trigger when the publish button is disabled', () => {
    render(<AppPublisher disabled publishedAt={Date.now()} onToggle={mockOnToggle} />)

    fireEvent.click(
      screen.getByText(/(?:^|\.)common\.publish(?=$|:)/).parentElement
        ?.parentElement as HTMLElement,
    )

    expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument()
    expect(mockOnToggle).not.toHaveBeenCalled()
  })

  it('should publish from the keyboard shortcut and restore the popover state', async () => {
    const preventDefault = vi.fn()
    const onRestore = vi.fn().mockResolvedValue(undefined)
    mockOnPublish.mockResolvedValue(undefined)

    render(
      <AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} onRestore={onRestore} />,
    )

    expect(hotkeyMocks.hotkeys).toContain('Mod+Shift+P')
    hotkeyMocks.handlers[0]!({ preventDefault })

    await waitFor(() => {
      expect(preventDefault).toHaveBeenCalled()
      expect(mockOnPublish).toHaveBeenCalledTimes(1)
    })

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-summary-restore'))

    await waitFor(() => {
      expect(onRestore).toHaveBeenCalledTimes(1)
    })
    expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument()
  })

  it('should keep the popover open when restore fails and reset published state after publish failures', async () => {
    const preventDefault = vi.fn()
    const onRestore = vi.fn().mockRejectedValue(new Error('restore failed'))
    mockOnPublish.mockRejectedValueOnce(new Error('publish failed'))

    render(
      <AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} onRestore={onRestore} />,
    )

    hotkeyMocks.handlers[0]!({ preventDefault })

    await waitFor(() => {
      expect(preventDefault).toHaveBeenCalled()
      expect(mockOnPublish).toHaveBeenCalledTimes(1)
    })
    expect(mockTrackEvent).not.toHaveBeenCalled()

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-summary-restore'))

    await waitFor(() => {
      expect(onRestore).toHaveBeenCalledTimes(1)
    })
    expect(screen.getByText('publisher-summary-publish'))!.toBeInTheDocument()
  })

  it('should report missing explore installations', async () => {
    mockFetchInstalledAppList.mockResolvedValueOnce({
      installed_apps: [],
    })
    mockOpenAsyncWindow.mockImplementation(
      async (resolver: () => Promise<string>, options: { onError: (error: Error) => void }) => {
        try {
          await resolver()
        } catch (error) {
          options.onError(error as Error)
        }
      },
    )

    render(<AppPublisher publishedAt={Date.now()} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-open-in-explore'))

    await waitFor(() => {
      expect(mockToastError).toHaveBeenCalledWith(
        expect.stringMatching(/(?:^|\.)notPublishedYet(?=$|:)/),
      )
    })
  })

  it('should report explore errors when the app cannot be opened', async () => {
    mockAppDetail = {
      ...mockAppDetail,
      id: undefined,
    }
    mockOpenAsyncWindow.mockImplementation(
      async (resolver: () => Promise<string>, options: { onError: (error: Error) => void }) => {
        try {
          await resolver()
        } catch (error) {
          options.onError(error as Error)
        }
      },
    )

    render(<AppPublisher publishedAt={Date.now()} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-open-in-explore'))

    await waitFor(() => {
      expect(mockToastError).toHaveBeenCalledWith('App not found')
    })
  })

  it('should show marketplace button and open redirect URL on success', async () => {
    mockPublishToCreatorsPlatform.mockResolvedValue({
      redirect_url: 'https://marketplace.example.com/publish?code=abc',
    })
    const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null)

    renderWithConsoleQuery(<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} />, {
      systemFeatures: { webapp_auth: { enabled: true }, enable_creators_platform: true },
    })

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/))

    await waitFor(() => {
      expect(mockPublishToCreatorsPlatform).toHaveBeenCalledWith({ appID: 'app-1' })
      expect(windowOpenSpy).toHaveBeenCalledWith(
        'https://marketplace.example.com/publish?code=abc',
        '_blank',
      )
    })

    windowOpenSpy.mockRestore()
  })

  it('should show toast error when publish to marketplace fails', async () => {
    mockPublishToCreatorsPlatform.mockRejectedValue(new Error('network error'))

    renderWithConsoleQuery(<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} />, {
      systemFeatures: { webapp_auth: { enabled: true }, enable_creators_platform: true },
    })

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/))

    await waitFor(() => {
      expect(mockToastError).toHaveBeenCalledWith(
        expect.stringMatching(/(?:^|\.)common\.publishToMarketplaceFailed(?=$|:)/),
      )
    })
  })

  it('should disable marketplace button when not yet published', () => {
    renderWithConsoleQuery(<AppPublisher onPublish={mockOnPublish} />, {
      systemFeatures: { webapp_auth: { enabled: true }, enable_creators_platform: true },
    })

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    const marketplaceButton = screen
      .getByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/)
      .closest('a, button, div[role="button"]') as HTMLElement
    expect(marketplaceButton).toBeInTheDocument()
    // clicking should not call the API because publishedAt is undefined
    fireEvent.click(screen.getByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/))
    expect(mockPublishToCreatorsPlatform).not.toHaveBeenCalled()
  })

  it('should hide marketplace button when enable_creators_platform is false', () => {
    render(<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    expect(
      screen.queryByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/),
    ).not.toBeInTheDocument()
  })

  it('should keep access control open when app detail is unavailable during confirmation', async () => {
    mockAppDetail = null

    render(<AppPublisher publishedAt={Date.now()} />)

    fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
    fireEvent.click(screen.getByText('publisher-access-control'))
    fireEvent.click(screen.getByText('confirm-access-control'))

    await waitFor(() => {
      expect(mockFetchAppDetail).not.toHaveBeenCalled()
    })
    expect(screen.getByTestId('access-control'))!.toBeInTheDocument()
  })
})
