import {encodeShortId, decodeShortId, deserializeShortId, serializeShortId} from '../short-id.js'

test('encodes the worked example from docs/handles-and-shortids.md', () => {
    const token = encodeShortId([0, 7, 42])
    expect(token).toBe('~DyU')
})

test('kind ids are scoped per arity: id 0 means item (arity 2) or category (arity 1)', () => {
    expect(deserializeShortId(decodeShortId(encodeShortId([0, 7, 42])))).toEqual({
        kind: 'item', owner_identity_id: 7, item_local_id: 42
    })
    expect(deserializeShortId(decodeShortId(encodeShortId([0, 5])))).toEqual({
        kind: 'category', category_id: 5
    })
})

test('every kind round-trips through serialize/encode/decode/deserialize', () => {
    const cases = [
        {kind: 'item', owner_identity_id: 7, item_local_id: 42},
        {kind: 'category', category_id: 5},
        {kind: 'group_item', owner_group_id: 5, item_local_id: 42},
        {kind: 'group', group_id: 11},
        {kind: 'storage_location', owner_identity_id: 3, storage_location_id: 1000},
        {kind: 'file', file_id: 123},
        {kind: 'workflow', owner_identity_id: 2, workflow_id: 9},
    ]
    for (const named of cases) {
        const token = encodeShortId(serializeShortId(named))
        expect(deserializeShortId(decodeShortId(token))).toEqual(named)
    }
})

test('a non-last field may be 0 - only the last field is restricted', () => {
    const named = {kind: 'item', owner_identity_id: 0, item_local_id: 42}
    const token = encodeShortId(serializeShortId(named))
    expect(deserializeShortId(decodeShortId(token))).toEqual(named)
})

test('rejects a 0 last-field value, for a single-field kind', () => {
    expect(() => encodeShortId(serializeShortId({kind: 'category', category_id: 0}))).toThrow()
})

test('rejects a 0 last-field value, for a multi-field kind', () => {
    expect(() => encodeShortId(serializeShortId({kind: 'item', owner_identity_id: 7, item_local_id: 0}))).toThrow()
})

test('decoding never over-reads: a single-chunk field that exactly exhausts padding is not ' +
    'mistaken for a second field', () => {
    // category_id 5 -> 7 bits + 5 zero padding bits, identical to a genuine one-chunk zero field - the case the last-field-nonzero rule disambiguates.
    const token = encodeShortId([0, 5])
    expect(token).toBe('~Cg')
    expect(decodeShortId(token)).toEqual([0, 5])
})

test('round-trips small and large nonzero values, in both last and non-last field position', () => {
    const values = [1, 7, 42, 100, 10000, 100000, 123456789]
    for (const value of values) {
        expect(decodeShortId(encodeShortId([0, value]))).toEqual([0, value])
        expect(decodeShortId(encodeShortId([0, value, 1]))).toEqual([0, value, 1])
    }
})

test('tokens are URL-safe: only unreserved characters, plus the leading ~', () => {
    const token = encodeShortId([0, 123456, 987654])
    expect(token[0]).toBe('~')
    expect(token.slice(1)).toMatch(/^[A-Za-z0-9\-_]+$/)
})

test('kinds at escaped ids (3+) round-trip through the escape tag, not just the direct 0-2 range', () => {
    const token = encodeShortId(serializeShortId({kind: 'workflow', owner_identity_id: 2, workflow_id: 9}))
    expect(deserializeShortId(decodeShortId(token))).toEqual({
        kind: 'workflow', owner_identity_id: 2, workflow_id: 9
    })
})

test('serializeShortId then deserializeShortId round-trips through encode/decode', () => {
    const token = encodeShortId(serializeShortId({kind: 'item', owner_identity_id: 7, item_local_id: 42}))
    expect(token).toBe('~DyU')
    expect(deserializeShortId(decodeShortId(token))).toEqual({
        kind: 'item',
        owner_identity_id: 7,
        item_local_id: 42
    })
})

test('rejects a token missing the leading ~', () => {
    expect(() => decodeShortId('DyU')).toThrow()
})

test('rejects an unknown kind index', () => {
    expect(() => encodeShortId([99, 1])).toThrow()
})

test('rejects an arity with no registered schema at that kind index', () => {
    // kind 0 is registered for arity 2 (item) and arity 1 (category), but not arity 3.
    expect(() => encodeShortId([0, 7, 8, 9])).toThrow()
})

test('rejects an unknown kind name', () => {
    expect(() => serializeShortId({kind: 'not_a_kind'})).toThrow()
})

test('rejects a missing field', () => {
    expect(() => serializeShortId({kind: 'item', owner_identity_id: 7})).toThrow()
})

test('rejects a negative field value', () => {
    expect(() => encodeShortId([0, -1, 42])).toThrow()
})
