import assert from 'node:assert/strict'

import { test } from 'vitest'

import {
  findCurrentGroupMember,
  getRecord,
  isInteractableMember,
  memberDetails,
  providerNameOf,
  rebindMemberOccurrence,
  rebindNode,
  resolveMember,
  selectGlobalChainNodes,
  selectRuleChainMembers,
  toMemberOccurrenceBinding,
} from '@/types/proxy-view'

const node = {
  recordId: 'p:0:0',
  name: 'provider-node',
  type: 'Shadowsocks',
  alive: true,
  history: [],
  udp: true,
  xudp: false,
  tfo: false,
  mptcp: false,
  smux: false,
  source: {
    kind: 'provider',
    providerName: 'provider-key',
    proxyName: 'provider-node',
  },
}

const group = {
  name: 'Group',
  type: 'Selector',
  alive: true,
  history: [],
  udp: true,
  xudp: false,
  tfo: false,
  mptcp: false,
  smux: false,
  members: [{ kind: 'node', name: node.name, recordId: node.recordId }],
}

const view = {
  schemaVersion: 1,
  orderSource: 'runtime',
  providerState: 'ready',
  global: null,
  direct: null,
  groups: [group],
  records: { [node.recordId]: node },
  standalone: [],
  providers: [],
}

test('resolves nodes only through recordId and preserves provider identity', () => {
  const resolved = resolveMember(view, group.members[0])
  assert.equal(resolved.kind, 'node')
  assert.equal(getRecord(view, 'p:0:0'), node)
  assert.equal(memberDetails(resolved), node)
  assert.equal(providerNameOf(node), 'provider-key')
  assert.equal(isInteractableMember(resolved), true)
})

test('keeps unresolved members visible but non-interactable', () => {
  const resolved = resolveMember(view, {
    kind: 'unresolved',
    name: 'unknown',
    reason: 'provider-unavailable',
  })
  assert.deepEqual(resolved, {
    kind: 'unresolved',
    ref: {
      kind: 'unresolved',
      name: 'unknown',
      reason: 'provider-unavailable',
    },
  })
  assert.equal(isInteractableMember(resolved), false)
  assert.equal(memberDetails(resolved), undefined)
})

test('rebinds a node semantically when its response-scoped id moves', () => {
  const previous = resolveMember(view, group.members[0])
  assert.equal(previous.kind, 'node')

  const movedNode = { ...node, recordId: 'p:1:0' }
  const movedGroup = {
    ...group,
    members: [
      { kind: 'node', name: movedNode.name, recordId: movedNode.recordId },
    ],
  }
  const movedView = {
    ...view,
    groups: [movedGroup],
    records: { [movedNode.recordId]: movedNode },
  }
  const movedCandidates = movedGroup.members.map((member) => {
    const resolved = resolveMember(movedView, member)
    assert.equal(resolved.kind, 'node')
    return resolved.node
  })

  assert.equal(
    rebindNode(movedCandidates, {
      name: previous.node.name,
      source: previous.node.source,
    })?.recordId,
    'p:1:0',
  )
  assert.equal(
    rebindNode(movedCandidates, { name: 'provider-node' })?.recordId,
    'p:1:0',
  )

  const duplicate = { ...movedNode, recordId: 'p:1:1' }
  assert.equal(
    rebindNode([movedNode, duplicate], {
      name: previous.node.name,
      source: previous.node.source,
    }),
    undefined,
  )

  const sameNameOtherSource = {
    ...movedNode,
    recordId: 'p:2:0',
    source: { ...movedNode.source, providerName: 'other-provider' },
  }
  assert.equal(
    rebindNode([movedNode, sameNameOtherSource], {
      name: 'provider-node',
    }),
    undefined,
  )
})

test('does not expose global-chain candidates without GLOBAL', () => {
  const runtimeOnly = {
    ...node,
    recordId: 'c:0',
    name: 'runtime-only',
    source: { kind: 'core', proxyName: 'runtime-only' },
  }
  const globalMissingView = {
    ...view,
    global: null,
    records: { 'c:0': runtimeOnly },
    standalone: ['c:0'],
  }

  assert.deepEqual(
    selectGlobalChainNodes(globalMissingView, [{ name: 'runtime-only' }]),
    [],
  )
})

test('does not fall back to global nodes when a selected rule group disappears', () => {
  assert.deepEqual(selectRuleChainMembers(view, 'removed-group'), [])
})

test('uses the current backend group selection after ids and now change', () => {
  const previousNode = {
    ...node,
    recordId: 'p:0:0',
    name: 'previous-node',
    source: { ...node.source, proxyName: 'previous-node' },
  }
  const nextNode = {
    ...node,
    recordId: 'p:0:1',
    name: 'next-node',
    source: { ...node.source, proxyName: 'next-node' },
  }
  const firstGroup = {
    ...group,
    now: previousNode.name,
    members: [
      {
        kind: 'node',
        name: previousNode.name,
        recordId: previousNode.recordId,
      },
      { kind: 'node', name: nextNode.name, recordId: nextNode.recordId },
    ],
  }
  const firstView = {
    ...view,
    groups: [firstGroup],
    records: {
      [previousNode.recordId]: previousNode,
      [nextNode.recordId]: nextNode,
    },
  }

  assert.equal(
    findCurrentGroupMember(firstView, firstGroup).member.node.recordId,
    previousNode.recordId,
  )

  const movedPrevious = { ...previousNode, recordId: 'p:1:1' }
  const movedNext = { ...nextNode, recordId: 'p:1:0' }
  const refreshedGroup = {
    ...firstGroup,
    now: movedNext.name,
    members: [
      { kind: 'node', name: movedNext.name, recordId: movedNext.recordId },
      {
        kind: 'node',
        name: movedPrevious.name,
        recordId: movedPrevious.recordId,
      },
    ],
  }
  const refreshedView = {
    ...firstView,
    groups: [refreshedGroup],
    records: {
      [movedNext.recordId]: movedNext,
      [movedPrevious.recordId]: movedPrevious,
    },
  }

  const current = findCurrentGroupMember(refreshedView, refreshedGroup)
  assert.equal(current.member.ref.name, movedNext.name)
  assert.equal(current.member.node.recordId, movedNext.recordId)

  const unresolvedGroup = {
    ...refreshedGroup,
    now: 'unresolved-node',
    members: [
      {
        kind: 'unresolved',
        name: 'unresolved-node',
        reason: 'provider-unavailable',
      },
      ...refreshedGroup.members,
    ],
  }
  assert.equal(
    findCurrentGroupMember(
      { ...refreshedView, groups: [unresolvedGroup] },
      unresolvedGroup,
    ),
    undefined,
  )
  assert.equal(
    findCurrentGroupMember(refreshedView, {
      ...refreshedGroup,
      now: undefined,
    }),
    undefined,
  )
  assert.equal(
    findCurrentGroupMember(refreshedView, {
      ...refreshedGroup,
      now: 'missing-node',
    }),
    undefined,
  )
})

test('rebinds a semantic member occurrence across id and index rotation', () => {
  const firstDuplicate = node
  const secondDuplicate = { ...node, recordId: 'p:0:1' }
  const firstMembers = [
    resolveMember(view, group.members[0]),
    {
      kind: 'group',
      ref: { kind: 'group', name: group.name },
      group,
    },
    {
      kind: 'node',
      ref: {
        kind: 'node',
        name: secondDuplicate.name,
        recordId: secondDuplicate.recordId,
      },
      node: secondDuplicate,
    },
  ]

  const binding = toMemberOccurrenceBinding(firstMembers, 2)
  assert.deepEqual(binding, {
    kind: 'node',
    name: node.name,
    source: node.source,
    occurrence: 1,
  })
  const groupBinding = toMemberOccurrenceBinding(firstMembers, 1)
  assert.deepEqual(groupBinding, {
    kind: 'group',
    name: group.name,
    occurrence: 0,
  })

  const movedFirst = { ...firstDuplicate, recordId: 'p:1:0' }
  const movedSecond = { ...secondDuplicate, recordId: 'p:1:1' }
  const refreshedMembers = [
    {
      kind: 'group',
      ref: { kind: 'group', name: group.name },
      group,
    },
    {
      kind: 'node',
      ref: {
        kind: 'node',
        name: movedFirst.name,
        recordId: movedFirst.recordId,
      },
      node: movedFirst,
    },
    {
      kind: 'unresolved',
      ref: { kind: 'unresolved', name: node.name, reason: 'ambiguous' },
    },
    {
      kind: 'node',
      ref: {
        kind: 'node',
        name: movedSecond.name,
        recordId: movedSecond.recordId,
      },
      node: movedSecond,
    },
  ]

  const rebound = rebindMemberOccurrence(refreshedMembers, binding)
  assert.equal(rebound.memberIndex, 3)
  assert.equal(rebound.member.node.recordId, movedSecond.recordId)
  assert.equal(
    rebindMemberOccurrence(refreshedMembers, groupBinding).memberIndex,
    0,
  )
})

test('rejects missing, unresolved, or missing duplicate occurrences', () => {
  const firstDuplicate = resolveMember(view, group.members[0])
  const secondNode = { ...node, recordId: 'p:0:1' }
  const secondDuplicate = {
    kind: 'node',
    ref: {
      kind: 'node',
      name: secondNode.name,
      recordId: secondNode.recordId,
    },
    node: secondNode,
  }
  const binding = toMemberOccurrenceBinding(
    [firstDuplicate, secondDuplicate],
    1,
  )

  assert.equal(
    rebindMemberOccurrence(
      [
        {
          kind: 'unresolved',
          ref: { kind: 'unresolved', name: node.name, reason: 'ambiguous' },
        },
      ],
      binding,
    ),
    undefined,
  )
  assert.equal(rebindMemberOccurrence([firstDuplicate], binding), undefined)
  assert.equal(
    toMemberOccurrenceBinding(
      [
        {
          kind: 'unresolved',
          ref: { kind: 'unresolved', name: node.name, reason: 'missing' },
        },
      ],
      0,
    ),
    undefined,
  )
})
