// Copyright 2017-2025 @pezkuwi/types-codec authors & contributors
// SPDX-License-Identifier: Apache-2.0

/// <reference types="@pezkuwi/dev-test/globals.d.ts" />

import { TypeRegistry } from '@pezkuwi/types';
import { bool, Bytes, Null, Option, Text, U32 } from '@pezkuwi/types-codec';

const registry = new TypeRegistry();

const testDecode = (type: string, input: any, expected: string): void =>
  it(`can decode from ${type}`, (): void => {
    const o = new Option(registry, Text, input);

    expect(o.toString()).toBe(expected);
    expect(o.isNone).toBe(!expected.length);
  });

const testEncode = (to: 'toHex' | 'toString' | 'toU8a', expected: any): void =>
  it(`can encode ${to}`, (): void => {
    const e = new Option(registry, Text, 'foo');

    expect(e[to]()).toEqual(expected);
  });

describe('Option', (): void => {
  it('converts undefined/null to empty', (): void => {
    expect(new Option(registry, Text, undefined).isNone).toBe(true);
    expect(new Option(registry, Text, null).isNone).toBe(true);
    expect(new Option(registry, Text, 'test').isNone).toBe(false);
    expect(new Option(registry, Text, '0x').isNone).toBe(true);
    expect(new Option(registry, '()', null).isNone).toBe(true);
  });

  it('can wrap an Option<Null>/Option<()>', (): void => {
    [
      new Option(registry, Null, new Null(registry)),
      new Option(registry, '()', new Null(registry))
    ].forEach((test): void => {
      expect(test.isSome).toBe(true);
      expect(test.isNone).toBe(false);
      expect(test.isEmpty).toBe(false);
      expect(test.toU8a()).toEqual(new Uint8Array([1]));
      expect(test.unwrap().toHex()).toEqual('0x');
    });
  });

  it('can decode a nested Option', (): void => {
    expect(
      new Option(
        registry,
        Option.with(Option.with(Text)),
        new Option(
          registry,
          Option.with(Text),
          new Option(
            registry,
            Text,
            new Uint8Array([1, 3 << 2, 66, 67, 68])
          )
        )
      ).toU8a()
    ).toEqual(new Uint8Array([1, 1, 1, 3 << 2, 66, 67, 68]));
  });

  it('can convert between different Some/None', (): void => {
    const def = '{ "foo":"Text", "zar":"Text" }';
    const none = new Option(registry, def, null);
    const some = new Option(registry, def, new Option(registry, def, { foo: 'a', zar: 'b' }));

    expect(new Option(registry, def, none).isNone).toBe(true);
    expect(new Option(registry, def, some).isNone).toBe(false);
    expect(new Option(registry, def, some).unwrap().toHuman()).toEqual({ foo: 'a', zar: 'b' });
  });

  it('correctly handles booleans', (): void => {
    expect(new Option(registry, bool).isNone).toBe(true);
    expect(new Option(registry, bool, true).isSome).toBe(true);
    expect(new Option(registry, bool, true).unwrap().isTrue).toBe(true);
    expect(new Option(registry, bool, false).isSome).toBe(true);
    expect(new Option(registry, bool, false).unwrap().isTrue).toBe(false);
  });

  it('converts an option to an option', (): void => {
    expect(
      new Option(registry, Text, new Option(registry, Text, 'hello')).toString()
    ).toEqual('hello');
  });

  it('converts an option to an option (strings)', (): void => {
    expect(
      new Option(registry, 'Text', new Option(registry, 'Text', 'hello')).toString()
    ).toEqual('hello');
  });

  it('converts correctly from hex with toHex (Bytes)', (): void => {
    // Option<Bytes> for a teyrchain head, however, this is effectively an
    // Option<Option<Bytes>> (hence the length, since it is from storage)
    const HEX = '0x210100000000000000000000000000000000000000000000000000000000000000000000000000000000011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce';

    // watch the hex prefix and length
    expect(
      new Option(registry, Bytes, HEX).toHex().substring(6)
    ).toEqual(HEX.substring(2));
  });

  it('converts correctly from hex with toNumber (U64)', (): void => {
    const HEX = '0x12345678';

    expect(
      new Option(registry, U32, HEX).unwrap().toNumber()
    ).toEqual(0x12345678);
  });

  it('decodes reusing instanciated inputs', (): void => {
    const foo = new Text(registry, 'bar');

    expect(
      (new Option(registry, Text, foo)).value
    ).toBe(foo);
  });

  testDecode('string (with)', 'foo', 'foo');
  testDecode('string (without)', undefined, '');
  testDecode('Uint8Array (with)', Uint8Array.from([1, 12, 102, 111, 111]), 'foo');
  testDecode('Uint8Array (without)', Uint8Array.from([0]), '');

  testEncode('toHex', '0x0c666f6f');
  testEncode('toString', 'foo');
  testEncode('toU8a', Uint8Array.from([1, 12, 102, 111, 111]));

  it('has empty toString() (undefined)', (): void => {
    expect(
      new Option(registry, Text).toString()
    ).toEqual('');
  });

  it('has value toString() (provided)', (): void => {
    expect(
      new Option(registry, Text, new Uint8Array([1, 4 << 2, 49, 50, 51, 52])).toString()
    ).toEqual('1234');
  });

  it('converts toU8a() with', (): void => {
    expect(
      new Option(registry, Text, '1234').toU8a()
    ).toEqual(new Uint8Array([1, 4 << 2, 49, 50, 51, 52]));
  });

  it('allows bare specifiers on toU8a', (): void => {
    expect(
      new Option(registry, Text, '1234').toU8a(true)
    ).toEqual(new Uint8Array([49, 50, 51, 52]));
  });

  it('converts toU8a() without', (): void => {
    expect(
      new Option(registry, Text).toU8a()
    ).toEqual(new Uint8Array([0]));
  });

  it('converts toJSON() as null without', (): void => {
    expect(
      new Option(registry, Text).toJSON()
    ).toEqual(null);
  });

  it('converts toJSON() as non-null with Bytes', (): void => {
    expect(
      new Option(registry, Bytes, 'abcde').toJSON()
    ).toEqual('0x6162636465');
  });

  it('converts toJSON() as non-null with Text', (): void => {
    expect(
      new Option(registry, Text, 'abcde').toJSON()
    ).toEqual('abcde');
  });

  describe('utils', (): void => {
    const test = new Option(registry, Text, '1234');

    it('compares against other option', (): void => {
      expect(test.eq(new Option(registry, Text, '1234'))).toBe(true);
    });

    it('compares against raw value', (): void => {
      expect(test.eq('1234')).toBe(true);
    });

    it('unwrapOr to specified if empty', (): void => {
      expect(new Option(registry, Text).unwrapOr('6789').toString()).toEqual('6789');
    });

    it('unwrapOr to specified if non-empty', (): void => {
      expect(new Option(registry, Text, '1234').unwrapOr(null)?.toString()).toEqual('1234');
    });

    it('unwrapOrDefault to default if empty', (): void => {
      expect(new Option(registry, U32).unwrapOrDefault().toNumber()).toEqual(0);
    });

    it('unwrapOrDefault to specified if non-empty', (): void => {
      expect(new Option(registry, U32, '1234').unwrapOrDefault().toNumber()).toEqual(1234);
    });

    it('has a sane inspect', (): void => {
      expect(
        new Option(registry, U32, '1234').inspect()
      ).toEqual({
        inner: undefined,
        outer: [new Uint8Array([0x01]), new Uint8Array([210, 4, 0, 0])]
      });
    });
  });
});
