import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models/access-control'
import type { App } from '@/types/app'
import { toast } from '@langgenius/dify-ui/toast'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import useAccessControlStore from '@/context/access-control-store'
import { AccessMode, SubjectType } from '@/models/access-control'
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
import AccessControlDialog from '../access-control-dialog'
import AddMemberOrGroupDialog from '../add-member-or-group-pop'
import AccessControl from '../index'
import SpecificGroupsOrMembers from '../specific-groups-or-members'

const mockUseAppWhiteListSubjects = vi.fn()
const mockUseSearchForWhiteListCandidates = vi.fn()
const { mockMutateAsync } = vi.hoisted(() => ({
  mockMutateAsync: vi.fn(),
}))
const intersectionObserverMocks = vi.hoisted(() => ({
  callback: null as null | ((entries: Array<{ isIntersecting: boolean }>) => void),
}))

vi.mock('@/service/access-control', () => ({
  useAppWhiteListSubjects: (...args: unknown[]) => mockUseAppWhiteListSubjects(...args),
  useSearchForWhiteListCandidates: (...args: unknown[]) =>
    mockUseSearchForWhiteListCandidates(...args),
}))

vi.mock('@/service/client', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@/service/client')>()
  const webAppAuth = new Proxy(actual.consoleQuery.enterprise.webAppAuth, {
    get(target, property, receiver) {
      if (property === 'updateWebAppWhitelistSubjects')
        return { mutationOptions: () => ({ mutationFn: mockMutateAsync }) }
      return Reflect.get(target, property, receiver)
    },
  })
  const enterprise = new Proxy(actual.consoleQuery.enterprise, {
    get(target, property, receiver) {
      if (property === 'webAppAuth') return webAppAuth
      return Reflect.get(target, property, receiver)
    },
  })

  return {
    ...actual,
    consoleQuery: new Proxy(actual.consoleQuery, {
      get(target, property, receiver) {
        if (property === 'enterprise') return enterprise
        return Reflect.get(target, property, receiver)
      },
    }),
  }
})

vi.mock('@/context/account-state', async () => {
  const { atom } = await vi.importActual<typeof import('jotai')>('jotai')
  return {
    userProfileAtom: atom({
      id: 'user-1',
      name: 'Test User',
      email: 'test@dify.ai',
      avatar: '',
      avatar_url: null,
      is_password_set: false,
      timezone: 'UTC',
    }),
  }
})

vi.mock('ahooks', async (importOriginal) => {
  const actual = await importOriginal<typeof import('ahooks')>()
  return {
    ...actual,
    useDebounce: (value: unknown) => value,
  }
})

const createGroup = (overrides: Partial<AccessControlGroup> = {}): AccessControlGroup =>
  ({
    id: 'group-1',
    name: 'Group One',
    groupSize: 5,
    ...overrides,
  }) as AccessControlGroup

const createMember = (overrides: Partial<AccessControlAccount> = {}): AccessControlAccount =>
  ({
    id: 'member-1',
    name: 'Member One',
    email: 'member@example.com',
    avatar: '',
    avatarUrl: '',
    ...overrides,
  }) as AccessControlAccount

const baseGroup = createGroup()
const baseMember = createMember()
const groupSubject: Subject = {
  subjectId: baseGroup.id,
  subjectType: SubjectType.GROUP,
  groupData: baseGroup,
} as Subject
const memberSubject: Subject = {
  subjectId: baseMember.id,
  subjectType: SubjectType.ACCOUNT,
  accountData: baseMember,
} as Subject

beforeAll(() => {
  class MockIntersectionObserver {
    constructor(callback: (entries: Array<{ isIntersecting: boolean }>) => void) {
      intersectionObserverMocks.callback = callback
    }

    observe = vi.fn(() => undefined)
    disconnect = vi.fn(() => undefined)
    unobserve = vi.fn(() => undefined)
  }
  // @ts-expect-error test DOM typings do not guarantee IntersectionObserver here
  globalThis.IntersectionObserver = MockIntersectionObserver
})

beforeEach(() => {
  vi.clearAllMocks()
  mockMutateAsync.mockResolvedValue(undefined)
  mockUseAppWhiteListSubjects.mockReturnValue({
    isPending: false,
    data: {
      groups: [baseGroup],
      members: [baseMember],
    },
  })
  mockUseSearchForWhiteListCandidates.mockReturnValue({
    isLoading: false,
    isFetchingNextPage: false,
    fetchNextPage: vi.fn(),
    data: { pages: [{ currPage: 1, subjects: [groupSubject, memberSubject], hasMore: false }] },
  })
})

// AccessControlDialog renders the shared dialog primitive with a close control.
describe('AccessControlDialog', () => {
  it('should render dialog content when visible', () => {
    render(
      <AccessControlDialog show className="custom-dialog">
        <div>Dialog Content</div>
      </AccessControlDialog>,
    )

    expect(screen.getByRole('dialog')).toBeInTheDocument()
    expect(screen.getByText('Dialog Content')).toBeInTheDocument()
  })

  it('should trigger onClose when clicking the close control', async () => {
    const handleClose = vi.fn()
    render(
      <AccessControlDialog show onClose={handleClose}>
        <div>Dialog Content</div>
      </AccessControlDialog>,
    )

    const closeButton = screen.getByRole('button', { name: 'Close' })
    fireEvent.click(closeButton)

    await waitFor(() => {
      expect(handleClose).toHaveBeenCalledTimes(1)
    })
  })
})

// SpecificGroupsOrMembers syncs store state with fetched data and supports removals
describe('SpecificGroupsOrMembers', () => {
  it('should render collapsed view when not in specific selection mode', () => {
    useAccessControlStore.setState({ currentMenu: AccessMode.ORGANIZATION })

    render(<SpecificGroupsOrMembers />)

    expect(screen.getByText('app.accessControlDialog.accessItems.specific')).toBeInTheDocument()
    expect(screen.queryByText(baseGroup.name)).not.toBeInTheDocument()
  })

  it('should show loading state while pending', async () => {
    useAccessControlStore.setState({
      appId: 'app-1',
      currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
    })
    mockUseAppWhiteListSubjects.mockReturnValue({
      isPending: true,
      data: undefined,
    })

    const { container } = render(<SpecificGroupsOrMembers />)

    await waitFor(() => {
      expect(container.querySelector('.spin-animation')).toBeInTheDocument()
    })
  })

  it('should render fetched groups and members and support removal', async () => {
    useAccessControlStore.setState({
      appId: 'app-1',
      currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
    })

    render(<SpecificGroupsOrMembers />)

    await waitFor(() => {
      expect(screen.getByText(baseGroup.name)).toBeInTheDocument()
      expect(screen.getByText(baseMember.name)).toBeInTheDocument()
    })

    const groupRemove = screen.getAllByRole('button', { name: /operation\.remove$/ })[0]!

    fireEvent.click(groupRemove)

    await waitFor(() => {
      expect(screen.queryByText(baseGroup.name)).not.toBeInTheDocument()
    })

    const memberRemove = screen.getAllByRole('button', { name: /operation\.remove$/ })[0]!

    fireEvent.click(memberRemove)

    await waitFor(() => {
      expect(screen.queryByText(baseMember.name)).not.toBeInTheDocument()
    })
  })
})

// AddMemberOrGroupDialog renders search results and updates store selections
describe('AddMemberOrGroupDialog', () => {
  it('should open search popover and display candidates', async () => {
    const user = userEvent.setup()

    render(<AddMemberOrGroupDialog />)

    await user.click(screen.getByText('common.operation.add'))

    expect(
      screen.getByPlaceholderText(
        'app.accessControlDialog.operateGroupAndMember.searchPlaceholder',
      ),
    ).toBeInTheDocument()
    expect(screen.getByText(baseGroup.name)).toBeInTheDocument()
    expect(screen.getByText(baseMember.name)).toBeInTheDocument()
  })

  it('should allow selecting members and expanding groups', async () => {
    const user = userEvent.setup()
    render(<AddMemberOrGroupDialog />)

    await user.click(screen.getByText('common.operation.add'))

    const expandButton = screen.getByText('app.accessControlDialog.operateGroupAndMember.expand')
    await user.click(expandButton)
    expect(useAccessControlStore.getState().selectedGroupsForBreadcrumb).toEqual([baseGroup])

    await user.click(screen.getByRole('option', { name: /Member One/ }))

    expect(useAccessControlStore.getState().specificMembers).toEqual([baseMember])
  })

  it('should update the keyword, fetch the next page, and support deselection and breadcrumb reset', async () => {
    const fetchNextPage = vi.fn()
    mockUseSearchForWhiteListCandidates.mockReturnValue({
      isLoading: false,
      isFetchingNextPage: true,
      fetchNextPage,
      data: { pages: [{ currPage: 1, subjects: [groupSubject, memberSubject], hasMore: true }] },
    })

    const user = userEvent.setup()
    render(<AddMemberOrGroupDialog />)

    await user.click(screen.getByText('common.operation.add'))
    await user.type(
      screen.getByPlaceholderText(
        'app.accessControlDialog.operateGroupAndMember.searchPlaceholder',
      ),
      'Group',
    )
    expect(document.querySelector('.spin-animation')).toBeInTheDocument()

    const groupOption = screen.getByRole('option', { name: /Group One/ })
    fireEvent.click(groupOption)
    fireEvent.click(groupOption)

    const memberOption = screen.getByRole('option', { name: /Member One/ })
    fireEvent.click(memberOption)
    fireEvent.click(memberOption)

    fireEvent.click(screen.getByText('app.accessControlDialog.operateGroupAndMember.expand'))
    fireEvent.click(screen.getByText('app.accessControlDialog.operateGroupAndMember.allMembers'))

    expect(useAccessControlStore.getState().specificGroups).toEqual([])
    expect(useAccessControlStore.getState().specificMembers).toEqual([])
    expect(useAccessControlStore.getState().selectedGroupsForBreadcrumb).toEqual([])
    expect(fetchNextPage).not.toHaveBeenCalled()
  })

  it('should show empty state when no candidates are returned', async () => {
    mockUseSearchForWhiteListCandidates.mockReturnValue({
      isLoading: false,
      isFetchingNextPage: false,
      fetchNextPage: vi.fn(),
      data: { pages: [] },
    })

    const user = userEvent.setup()
    render(<AddMemberOrGroupDialog />)

    await user.click(screen.getByText('common.operation.add'))

    expect(screen.getByRole('status')).toHaveTextContent(
      'app.accessControlDialog.operateGroupAndMember.noResult',
    )
  })
})

// AccessControl integrates dialog, selection items, and confirm flow
describe('AccessControl', () => {
  it('should initialize menu from app and call update on confirm', async () => {
    const onClose = vi.fn()
    const onConfirm = vi.fn()
    const toastSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success')
    useAccessControlStore.setState({
      specificGroups: [baseGroup],
      specificMembers: [baseMember],
    })
    const app = {
      id: 'app-id-1',
      access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
    } as App

    render(<AccessControl app={app} onClose={onClose} onConfirm={onConfirm} />)

    await waitFor(() => {
      expect(useAccessControlStore.getState().currentMenu).toBe(AccessMode.SPECIFIC_GROUPS_MEMBERS)
    })

    fireEvent.click(screen.getByText('common.operation.confirm'))

    await waitFor(() => {
      expect(mockMutateAsync.mock.calls[0]?.[0]).toEqual({
        body: {
          appId: app.id,
          accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
          subjects: [
            { subjectId: baseGroup.id, subjectType: SubjectType.GROUP },
            { subjectId: baseMember.id, subjectType: SubjectType.ACCOUNT },
          ],
        },
      })
      expect(toastSpy).toHaveBeenCalledWith('app.accessControlDialog.updateSuccess')
      expect(onConfirm).toHaveBeenCalled()
    })
  })

  it('should expose the external members tip when SSO is disabled', () => {
    const app = {
      id: 'app-id-2',
      access_mode: AccessMode.PUBLIC,
    } as App

    render(<AccessControl app={app} onClose={vi.fn()} />)

    expect(screen.getByText('app.accessControlDialog.accessItems.external')).toBeInTheDocument()
    expect(screen.getByText('app.accessControlDialog.accessItems.anyone')).toBeInTheDocument()
  })
})
