import { render, screen, fireEvent, act } from '@testing-library/react'
import {
  MembersTable,
  RoleSelector,
  InviteMemberDialog,
  PendingInvitationCallout,
  DomainClaimCard,
  LeaveTeamDialog,
  OrgSwitcher,
} from '../index'
import type { MemberRow, InvitationLite, DomainClaimLite, TeamLite, CompanyLite } from '../types'

// ---------- MembersTable ---------------------------------------------------

describe('MembersTable', () => {
  const baseMembers: MemberRow[] = [
    {
      id: 'm1',
      userId: 'u1',
      teamId: 't1',
      role: 'owner',
      joinedAt: '2025-01-01',
      user: { id: 'u1', email: 'owner@x.com', firstName: 'Olivia', lastName: 'Owner' },
    },
    {
      id: 'm2',
      userId: 'u2',
      teamId: 't1',
      role: 'member',
      joinedAt: '2025-01-02',
      user: { id: 'u2', email: 'mem@x.com', firstName: 'Mike', lastName: 'Member' },
    },
  ]

  it('renders a row per member with name + email + role', () => {
    render(<MembersTable members={baseMembers} />)
    expect(screen.getByText('Olivia Owner')).toBeInTheDocument()
    expect(screen.getByText('owner@x.com')).toBeInTheDocument()
    expect(screen.getAllByText('Owner').length).toBeGreaterThan(0)
    expect(screen.getByText('Mike Member')).toBeInTheDocument()
  })

  it('hides the actions column when canManage is false', () => {
    render(<MembersTable members={baseMembers} />)
    expect(screen.queryByText('Remove')).not.toBeInTheDocument()
  })

  it('shows RoleSelector + Remove when canManage is true', () => {
    const onChangeRole = jest.fn()
    const onRemove = jest.fn()
    render(
      <MembersTable
        members={baseMembers}
        canManage
        onChangeRole={onChangeRole}
        onRemove={onRemove}
      />
    )
    const removes = screen.getAllByText('Remove')
    expect(removes).toHaveLength(2)
    fireEvent.click(removes[1])
    expect(onRemove).toHaveBeenCalledWith('u2')

    const selects = screen.getAllByRole('combobox') as HTMLSelectElement[]
    fireEvent.change(selects[1], { target: { value: 'admin' } })
    expect(onChangeRole).toHaveBeenCalledWith('u2', 'admin')
  })

  it('respects classNames overrides', () => {
    const { container } = render(
      <MembersTable members={baseMembers} classNames={{ root: 'my-root-class' }} />
    )
    expect(container.firstChild).toHaveClass('my-root-class')
  })

  it('does not print the email twice when it is also the display name', () => {
    // Foundry maps control-plane members with an email and no name, so
    // userDisplayName() falls back to the email — the row used to stack the
    // same string on itself. startsim-1f3n.4.
    render(
      <MembersTable
        members={[
          {
            id: 'u1',
            userId: 'u1',
            teamId: 't1',
            role: 'owner',
            joinedAt: '2025-01-01',
            user: { id: 'u1', email: 'qosha@debugg.ai' },
          },
        ]}
      />
    )
    expect(screen.getAllByText('qosha@debugg.ai')).toHaveLength(1)
  })

  it('marks the signed-in user so they can tell which row is them', () => {
    // startsim-1f3n.7 — the owner appeared as an anonymous row next to a live
    // Remove button with nothing saying "this one is you".
    render(<MembersTable members={baseMembers} currentUserId="u2" />)
    expect(screen.getByText('You')).toBeInTheDocument()
    const selfRow = screen.getByText('Mike Member').closest('tr')!
    expect(selfRow).toContainElement(screen.getByText('You'))
  })

  it('renders no You badge when currentUserId matches nobody', () => {
    render(<MembersTable members={baseMembers} currentUserId="nobody" />)
    expect(screen.queryByText('You')).not.toBeInTheDocument()
  })

  it('keeps the Role and actions columns to their content width', () => {
    // startsim-1f3n.6 — one shared cell class meant Member ate ~55% of the
    // table and Role/Remove floated in a gap.
    render(<MembersTable members={baseMembers} canManage onChangeRole={jest.fn()} />)
    const roleHeader = screen.getByRole('columnheader', { name: 'Role' })
    expect(roleHeader.className).toContain('w-px')
    expect(roleHeader.className).toContain('whitespace-nowrap')
  })

  it('shows the empty state when no members', () => {
    render(<MembersTable members={[]} />)
    expect(screen.getByText('No members yet.')).toBeInTheDocument()
  })
})

// ---------- RoleSelector ---------------------------------------------------

describe('RoleSelector', () => {
  it('renders the supplied roles and fires onChange', () => {
    const onChange = jest.fn()
    render(
      <RoleSelector
        value="member"
        onChange={onChange}
        availableRoles={['admin', 'member', 'viewer']}
      />
    )
    const select = screen.getByRole('combobox') as HTMLSelectElement
    expect(select.value).toBe('member')
    // Only the three supplied roles should be rendered.
    const options = Array.from(select.querySelectorAll('option')).map((o) => o.value)
    expect(options).toEqual(['admin', 'member', 'viewer'])
    fireEvent.change(select, { target: { value: 'admin' } })
    expect(onChange).toHaveBeenCalledWith('admin')
  })

  it('honors disabled', () => {
    render(<RoleSelector value="viewer" onChange={() => {}} disabled />)
    expect(screen.getByRole('combobox')).toBeDisabled()
  })
})

// ---------- InviteMemberDialog --------------------------------------------

describe('InviteMemberDialog', () => {
  it('does not render the panel until the trigger is clicked', () => {
    render(<InviteMemberDialog teamId="t1" onSubmit={jest.fn()} />)
    expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
    fireEvent.click(screen.getByRole('button', { name: /invite member/i }))
    expect(screen.getByRole('dialog')).toBeInTheDocument()
  })

  it('submits email + role + teamId via onSubmit and shows a success line', async () => {
    const onSubmit = jest.fn().mockResolvedValue({ id: 'i1' } as InvitationLite)
    const onInvited = jest.fn()
    render(
      <InviteMemberDialog
        teamId="t1"
        onSubmit={onSubmit}
        onInvited={onInvited}
      />
    )
    fireEvent.click(screen.getByRole('button', { name: /invite member/i }))

    fireEvent.change(screen.getByLabelText('Email'), {
      target: { value: 'newhire@x.com' },
    })
    fireEvent.change(screen.getByLabelText('Invite role'), {
      target: { value: 'admin' },
    })

    await act(async () => {
      fireEvent.submit(screen.getByRole('dialog').querySelector('form')!)
    })

    expect(onSubmit).toHaveBeenCalledWith({
      email: 'newhire@x.com',
      teamId: 't1',
      role: 'admin',
    })
    expect(onInvited).toHaveBeenCalled()
    expect(screen.getByText(/Invitation sent to newhire@x.com/)).toBeInTheDocument()
  })

  it('surfaces submit errors inline', async () => {
    const onSubmit = jest.fn().mockRejectedValue(new Error('domain not allowed'))
    render(<InviteMemberDialog teamId="t1" onSubmit={onSubmit} />)
    fireEvent.click(screen.getByRole('button', { name: /invite member/i }))
    fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'a@x.com' } })

    await act(async () => {
      fireEvent.submit(screen.getByRole('dialog').querySelector('form')!)
    })
    expect(screen.getByRole('alert')).toHaveTextContent('domain not allowed')
  })

  it('honors classNames slots', () => {
    render(
      <InviteMemberDialog
        teamId="t1"
        onSubmit={jest.fn()}
        classNames={{ trigger: 'my-trigger' }}
      />
    )
    expect(screen.getByRole('button', { name: /invite member/i })).toHaveClass(
      'my-trigger'
    )
  })
})

// ---------- PendingInvitationCallout --------------------------------------

describe('PendingInvitationCallout', () => {
  const invitation: InvitationLite = {
    id: 'i1',
    email: 'a@x.com',
    teamId: 't1',
    role: 'admin',
    expiresAt: '2099-01-01',
    isExpired: false,
    isAccepted: false,
  }

  it('renders the message + team name + role', () => {
    render(
      <PendingInvitationCallout
        invitation={invitation}
        teamName="Acme Eng"
      />
    )
    expect(screen.getByText(/You.*invited/i)).toBeInTheDocument()
    expect(screen.getByText('Acme Eng')).toBeInTheDocument()
    expect(screen.getByText('admin')).toBeInTheDocument()
  })

  it('falls back to "team #<id>" when no team name is supplied', () => {
    render(<PendingInvitationCallout invitation={invitation} />)
    expect(screen.getByText('team #t1')).toBeInTheDocument()
  })

  it('calls onAccept + onAccepted', async () => {
    const onAccept = jest.fn().mockResolvedValue(undefined)
    const onAccepted = jest.fn()
    render(
      <PendingInvitationCallout
        invitation={invitation}
        teamName="Acme"
        onAccept={onAccept}
        onAccepted={onAccepted}
      />
    )
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /accept/i }))
    })
    expect(onAccept).toHaveBeenCalledWith(invitation)
    expect(onAccepted).toHaveBeenCalledWith(invitation)
  })

  it('only renders Decline when an onDecline is provided', () => {
    const { rerender } = render(
      <PendingInvitationCallout invitation={invitation} teamName="Acme" />
    )
    expect(screen.queryByRole('button', { name: /decline/i })).not.toBeInTheDocument()
    rerender(
      <PendingInvitationCallout
        invitation={invitation}
        teamName="Acme"
        onDecline={jest.fn()}
      />
    )
    expect(screen.getByRole('button', { name: /decline/i })).toBeInTheDocument()
  })
})

// ---------- DomainClaimCard ------------------------------------------------

describe('DomainClaimCard', () => {
  const baseClaim: DomainClaimLite = {
    id: 'd1',
    companyId: 'c1',
    domain: 'acme.com',
    verified: false,
    verificationToken: 'startsim-verify=xyz',
    createdAt: '2025-01-01',
  }

  it('renders the DNS TXT block when a verification token is present', () => {
    render(<DomainClaimCard claim={baseClaim} />)
    expect(screen.getByText('acme.com')).toBeInTheDocument()
    expect(screen.getByText('startsim-verify=xyz')).toBeInTheDocument()
    expect(screen.getByRole('button', { name: /verify dns/i })).toBeInTheDocument()
  })

  it('hides verification UI once verified=true', () => {
    render(<DomainClaimCard claim={{ ...baseClaim, verified: true }} />)
    expect(screen.getByText('Verified')).toBeInTheDocument()
    expect(screen.queryByRole('button', { name: /verify dns/i })).not.toBeInTheDocument()
  })

  it('initiate-email then submit-code calls the verify handler', async () => {
    const onInitiateEmail = jest.fn().mockResolvedValue(undefined)
    const onVerifyEmail = jest.fn().mockResolvedValue(undefined)
    render(
      <DomainClaimCard
        claim={baseClaim}
        onInitiateEmail={onInitiateEmail}
        onVerifyEmail={onVerifyEmail}
      />
    )
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /email attestation/i }))
    })
    expect(onInitiateEmail).toHaveBeenCalledWith(baseClaim)

    const code = screen.getByLabelText('Attestation code')
    fireEvent.change(code, { target: { value: '123456' } })
    await act(async () => {
      fireEvent.click(screen.getByRole('button', { name: /submit code/i }))
    })
    expect(onVerifyEmail).toHaveBeenCalledWith(baseClaim, '123456')
  })

  it('renders Revoke when onRevoke is provided', () => {
    render(<DomainClaimCard claim={baseClaim} onRevoke={jest.fn()} />)
    expect(screen.getByRole('button', { name: /revoke/i })).toBeInTheDocument()
  })
})

// ---------- LeaveTeamDialog ------------------------------------------------

describe('LeaveTeamDialog', () => {
  const team: TeamLite = { id: 't1', slug: 'eng', name: 'Engineering', companyId: 'c1' }

  it('opens on trigger click, fires onSubmit + onLeft on confirm', async () => {
    const onSubmit = jest.fn().mockResolvedValue(undefined)
    const onLeft = jest.fn()
    render(<LeaveTeamDialog team={team} onSubmit={onSubmit} onLeft={onLeft} />)
    fireEvent.click(screen.getByRole('button', { name: /leave team/i }))
    expect(screen.getByRole('dialog')).toBeInTheDocument()
    await act(async () => {
      // Click the confirm button (red); it's the second 'Leave team' on screen.
      fireEvent.click(screen.getAllByRole('button', { name: /leave team/i })[1])
    })
    expect(onSubmit).toHaveBeenCalledWith(team)
    expect(onLeft).toHaveBeenCalledWith(team)
  })

  it('surfaces backend "last owner" errors inline', async () => {
    const onSubmit = jest.fn().mockRejectedValue(new Error('Cannot remove the last owner'))
    render(<LeaveTeamDialog team={team} onSubmit={onSubmit} />)
    fireEvent.click(screen.getByRole('button', { name: /leave team/i }))
    await act(async () => {
      fireEvent.click(screen.getAllByRole('button', { name: /leave team/i })[1])
    })
    expect(screen.getByRole('alert')).toHaveTextContent(
      /cannot remove the last owner/i
    )
  })
})

// ---------- OrgSwitcher ----------------------------------------------------

describe('OrgSwitcher', () => {
  const companies: CompanyLite[] = [
    { id: 'c1', slug: 'acme', name: 'Acme' },
    { id: 'c2', slug: 'beta', name: 'Beta' },
  ]

  it('renders an option per company + fires onSwitch on change', () => {
    const onSwitch = jest.fn()
    render(
      <OrgSwitcher companies={companies} currentCompanyId="c1" onSwitch={onSwitch} />
    )
    const select = screen.getByRole('combobox') as HTMLSelectElement
    expect(select.value).toBe('c1')
    fireEvent.change(select, { target: { value: 'c2' } })
    expect(onSwitch).toHaveBeenCalledWith('c2')
  })

  it('renders nothing when there are no companies', () => {
    const { container } = render(
      <OrgSwitcher companies={[]} currentCompanyId={null} onSwitch={() => {}} />
    )
    expect(container.firstChild).toBeNull()
  })
})
