import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import type { DesktopConnectionsRegistry } from '@/global'
import { $connection } from '@/store/session'

import {
  ConnectionsRegistrySection,
  findDuplicateConnection,
  normalizeGatewayUrl,
  sameBackendPeerLabel,
  sshCompositeKey
} from './connections-registry'
import { renderWithEnglish as render } from './test-locale'

const list = vi.fn()
const save = vi.fn()
const remove = vi.fn()
const setLaunchMode = vi.fn()
const setPrimary = vi.fn()
const test = vi.fn()

const registry: DesktopConnectionsRegistry = {
  connections: [
    { id: 'local', kind: 'local', label: 'This device', tokenPreview: null, tokenSet: false },
    {
      authMode: 'token',
      id: 'homelab',
      kind: 'remote',
      label: 'Homelab',
      tokenPreview: '...abc123',
      tokenSet: true,
      url: 'http://homelab.lan:9119'
    }
  ],
  primary: 'local',
  secureTokenStorage: true,
  version: 2
}

beforeEach(() => {
  $connection.set({
    baseUrl: 'http://homelab.lan:9119',
    connectionId: 'homelab',
    isFullscreen: false,
    logs: [],
    mode: 'remote',
    nativeOverlayWidth: 0,
    token: 'test-token',
    windowButtonPosition: null,
    wsUrl: 'ws://homelab.lan:9119/ws'
  })
  list.mockResolvedValue(registry)
  save.mockResolvedValue({ connection: registry.connections[1], ok: true, registry })
  remove.mockResolvedValue({ ok: true, registry: { ...registry, connections: [registry.connections[0]] } })
  setLaunchMode.mockResolvedValue({ ok: true, registry: { ...registry, launchMode: 'last-used' } })
  setPrimary.mockResolvedValue({ ok: true, registry: { ...registry, primary: 'homelab' } })
  test.mockResolvedValue({ ok: true, reachable: true })
  Object.defineProperty(window, 'hermesDesktop', {
    configurable: true,
    value: { connections: { list, remove, save, setLaunchMode, setPrimary, test } }
  })
})

afterEach(() => {
  $connection.set(null)
  cleanup()
  vi.clearAllMocks()
})

describe('ConnectionsRegistrySection', () => {
  it('distinguishes the current connection from the registry primary', async () => {
    render(<ConnectionsRegistrySection />)

    await waitFor(() => expect(screen.getByText('Homelab')).toBeTruthy())
    // Label and the managed pill share the copy, so expect both instances.
    expect(screen.getAllByText('This device').length).toBeGreaterThan(0)
    expect(screen.getByText('Current')).toBeTruthy()
    expect(screen.getAllByText('Primary').length).toBeGreaterThan(0)
    expect(list).toHaveBeenCalledTimes(1)
  })

  it('opens the add-connection editor and saves with a required label', async () => {
    render(<ConnectionsRegistrySection />)

    await waitFor(() => expect(screen.getByText('Homelab')).toBeTruthy())
    fireEvent.click(screen.getByText('Add connection'))

    // Save is disabled until a label is present.
    const saveButton = screen.getByText('Save connection').closest('button')!
    expect(saveButton.disabled).toBe(true)

    fireEvent.change(screen.getByPlaceholderText('Homelab'), { target: { value: 'Spark box' } })
    fireEvent.change(screen.getByPlaceholderText('http://homelab.lan:9119'), {
      target: { value: 'http://spark.lan:9119' }
    })
    expect(saveButton.disabled).toBe(false)
    fireEvent.click(saveButton)

    await waitFor(() => expect(save).toHaveBeenCalledTimes(1))
    expect(save.mock.calls[0][0]).toMatchObject({
      kind: 'remote',
      label: 'Spark box',
      url: 'http://spark.lan:9119'
    })
  })

  it('offers every kind on create and disables Local while the managed entry exists', async () => {
    render(<ConnectionsRegistrySection />)

    await waitFor(() => expect(screen.getByText('Homelab')).toBeTruthy())
    fireEvent.click(screen.getByText('Add connection'))

    const localKind = screen.getByRole('button', { name: 'Local' }) as HTMLButtonElement
    expect(localKind.disabled).toBe(true)
    expect(screen.getByRole('button', { name: 'Hermes Cloud' })).toBeTruthy()
    expect(screen.getByRole('button', { name: 'Remote gateway' })).toBeTruthy()
    expect(screen.getByRole('button', { name: 'SSH' })).toBeTruthy()
  })

  it('rejects a duplicate gateway URL in the save path with an inline error', async () => {
    render(<ConnectionsRegistrySection />)

    await waitFor(() => expect(screen.getByText('Homelab')).toBeTruthy())
    fireEvent.click(screen.getByText('Add connection'))

    fireEvent.change(screen.getByPlaceholderText('Homelab'), { target: { value: 'Homelab twin' } })
    // Same URL modulo case + trailing slash: normalized-dupe of the existing entry.
    fireEvent.change(screen.getByPlaceholderText('http://homelab.lan:9119'), {
      target: { value: 'HTTP://HOMELAB.LAN:9119/' }
    })
    fireEvent.click(screen.getByText('Save connection').closest('button')!)

    await waitFor(() =>
      expect(screen.getByText('A connection to this gateway URL already exists (“Homelab”).')).toBeTruthy()
    )
    expect(save).not.toHaveBeenCalled()
  })

  it('keeps the primary fallback configurable while last-used restore is enabled', async () => {
    list.mockResolvedValueOnce({ ...registry, launchMode: 'last-used' })
    render(<ConnectionsRegistrySection />)

    await waitFor(() => expect(screen.getByText('Homelab')).toBeTruthy())
    const makePrimary = screen.getByText('Make primary').closest('button')!

    expect(makePrimary.disabled).toBe(false)
    fireEvent.click(makePrimary)

    await waitFor(() => expect(setPrimary).toHaveBeenCalledWith('homelab'))
  })

  it('lets users opt into restoring the last-used source', async () => {
    render(<ConnectionsRegistrySection />)

    const launchSetting = await screen.findByText('At startup, return to Sessions on the last-used gateway')
    const addConnection = screen.getByText('Add connection')

    expect(addConnection.compareDocumentPosition(launchSetting) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
    fireEvent.click(screen.getByRole('switch', { name: 'At startup, return to Sessions on the last-used gateway' }))

    await waitFor(() => expect(setLaunchMode).toHaveBeenCalledWith('last-used'))
  })

  it('offers the launch preference even for a single source', async () => {
    // A local-only registry is the drift state from #90174, and the launch
    // toggle is the control that lets a user out of it. Hiding it there left
    // hand-editing connections.json as the only recourse.
    list.mockResolvedValueOnce({ ...registry, connections: [registry.connections[0]] })

    render(<ConnectionsRegistrySection />)

    await waitFor(() => expect(list).toHaveBeenCalledTimes(1))
    expect(screen.getByText('At startup, return to Sessions on the last-used gateway')).toBeTruthy()
  })

  it('keeps search out of the way for a small registry', async () => {
    render(<ConnectionsRegistrySection />)

    await waitFor(() => expect(screen.getByText('Homelab')).toBeTruthy())
    expect(screen.queryByRole('searchbox', { name: 'Search gateways…' })).toBeNull()
  })

  it('sorts a large registry and searches names and endpoints', async () => {
    const largeRegistry: DesktopConnectionsRegistry = {
      ...registry,
      connections: [
        {
          authMode: 'token',
          id: 'zulu',
          kind: 'remote',
          label: 'Zulu',
          tokenPreview: null,
          tokenSet: false,
          url: 'https://zulu.example.test'
        },
        registry.connections[0],
        ...Array.from({ length: 6 }, (_, index) => ({
          authMode: 'token' as const,
          id: `gateway-${index}`,
          kind: 'remote' as const,
          label: index === 0 ? 'Alpha' : `Gateway ${index}`,
          tokenPreview: null,
          tokenSet: false,
          url:
            index === 4
              ? 'https://studio.example.test'
              : index === 5
                ? 'https://studio-archive.example.test'
                : `https://gateway-${index}.example.test`
        }))
      ]
    }

    list.mockResolvedValueOnce(largeRegistry)
    render(
      <div data-testid="settings-scroller" style={{ height: 400, overflowY: 'auto' }}>
        <ConnectionsRegistrySection />
      </div>
    )

    const search = await screen.findByRole('searchbox', { name: 'Search gateways…' })
    expect(search.parentElement?.className).toContain('mt-3')
    expect(search.parentElement?.className).toContain('mb-0')
    const settingsScroller = screen.getByTestId('settings-scroller')
    settingsScroller.scrollTop = 200
    vi.spyOn(search, 'getBoundingClientRect')
      .mockReturnValueOnce({
        bottom: 152,
        height: 32,
        left: 0,
        right: 0,
        top: 120,
        width: 0,
        x: 0,
        y: 120,
        toJSON: () => ({})
      })
      .mockReturnValueOnce({
        bottom: 152,
        height: 32,
        left: 0,
        right: 0,
        top: 120,
        width: 0,
        x: 0,
        y: 120,
        toJSON: () => ({})
      })
      .mockReturnValueOnce({
        bottom: 152,
        height: 32,
        left: 0,
        right: 0,
        top: 120,
        width: 0,
        x: 0,
        y: 120,
        toJSON: () => ({})
      })
      .mockReturnValue({
        bottom: 182,
        height: 32,
        left: 0,
        right: 0,
        top: 150,
        width: 0,
        x: 0,
        y: 150,
        toJSON: () => ({})
      })
    const alpha = screen.getByText('Alpha')
    const zulu = screen.getByText('Zulu')
    expect(alpha.compareDocumentPosition(zulu) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()

    fireEvent.change(search, { target: { value: 'studio' } })

    expect(settingsScroller.scrollTop).toBe(200)
    expect(screen.getByText('Gateway 4')).toBeTruthy()
    expect(screen.getByText('Gateway 5')).toBeTruthy()
    expect(screen.queryByText('Alpha')).toBeNull()

    settingsScroller.scrollTop = 260
    fireEvent.change(search, { target: { value: 'studio.example' } })
    expect(settingsScroller.scrollTop).toBe(290)
    expect(screen.getByText('Gateway 4')).toBeTruthy()
    expect(screen.queryByText('Gateway 5')).toBeNull()

    fireEvent.change(search, { target: { value: '' } })
    expect(search.closest<HTMLElement>('.border-t')?.style.minHeight).toBe('')
  })

  it('tests a connection through the bridge', async () => {
    render(<ConnectionsRegistrySection />)

    await waitFor(() => expect(screen.getByText('Homelab')).toBeTruthy())
    fireEvent.click(screen.getAllByText('Test')[0])

    await waitFor(() => expect(test).toHaveBeenCalled())
  })
})

describe('dedupe helpers', () => {
  it('normalizes gateway URLs (trim, trailing slashes, lowercase)', () => {
    expect(normalizeGatewayUrl(' HTTP://Homelab.LAN:9119// ')).toBe('http://homelab.lan:9119')
  })

  it('normalizes ssh composites and defaults the port', () => {
    expect(sshCompositeKey('alice@Box')).toBe('alice@box:22')
    expect(sshCompositeKey('alice@box:22')).toBe('alice@box:22')
    expect(sshCompositeKey('box:2222')).toBe('@box:2222')
    expect(sshCompositeKey('  ')).toBe('')
  })

  it('finds at most one local entry', () => {
    expect(
      findDuplicateConnection({ host: '', id: null, kind: 'local', remoteProfile: '', url: '' }, registry.connections)
    ).toMatchObject({ id: 'local' })
    // Editing the local entry itself is not a self-collision.
    expect(
      findDuplicateConnection(
        { host: '', id: 'local', kind: 'local', remoteProfile: '', url: '' },
        registry.connections
      )
    ).toBeNull()
  })

  it('keys remote/cloud dupes on the normalized URL across both kinds', () => {
    expect(
      findDuplicateConnection(
        { host: '', id: null, kind: 'cloud', remoteProfile: '', url: 'http://HOMELAB.lan:9119/' },
        registry.connections
      )
    ).toMatchObject({ id: 'homelab' })
    expect(
      findDuplicateConnection(
        { host: '', id: null, kind: 'remote', remoteProfile: '', url: 'http://other.lan:9119' },
        registry.connections
      )
    ).toBeNull()
    // Editing the entry itself is not a self-collision.
    expect(
      findDuplicateConnection(
        { host: '', id: 'homelab', kind: 'remote', remoteProfile: '', url: 'http://homelab.lan:9119' },
        registry.connections
      )
    ).toBeNull()
  })

  it('keys ssh dupes on user@host:port + remote profile', () => {
    const connections = [
      ...registry.connections,
      {
        host: 'box',
        id: 'box',
        kind: 'ssh' as const,
        label: 'Box',
        port: 22,
        remoteProfile: 'work',
        tokenPreview: null,
        tokenSet: false,
        user: 'alice'
      }
    ]

    expect(
      findDuplicateConnection(
        { host: 'alice@box:22', id: null, kind: 'ssh', remoteProfile: 'work', url: '' },
        connections
      )
    ).toMatchObject({ id: 'box' })
    // Different profile on the same host is a distinct agent source.
    expect(
      findDuplicateConnection(
        { host: 'alice@box:22', id: null, kind: 'ssh', remoteProfile: 'other', url: '' },
        connections
      )
    ).toBeNull()
  })

  it('hints "Same backend as" only on later rows sharing an install_id', () => {
    const spark = { id: 'spark', installId: 'aaa', label: 'Spark' }
    const sparkTs = { id: 'spark-ts', installId: 'aaa', label: 'Spark TS' }
    const mini = { id: 'mini', installId: 'bbb', label: 'Mini' }
    const legacy = { id: 'old', label: 'Old box' }
    const connections = [spark, sparkTs, mini, legacy]

    // The first occurrence carries no hint; the later duplicate points back.
    expect(sameBackendPeerLabel(spark, connections)).toBeNull()
    expect(sameBackendPeerLabel(sparkTs, connections)).toBe('Spark')
    // Unique ids and id-less (older backend) rows never hint.
    expect(sameBackendPeerLabel(mini, connections)).toBeNull()
    expect(sameBackendPeerLabel(legacy, connections)).toBeNull()
  })
})
