import * as bs58 from 'bs58';
import { describe, expect, it } from 'vitest';

import type { NormalizedHexString } from '@layerzerolabs/common-chain-model';
import { normalizeHex } from '@layerzerolabs/common-chain-model';
import { bytesToHexPrefixed, hexToBytes, hexZeroPad } from '@layerzerolabs/common-encoding-utils';
import { ChainName } from '@layerzerolabs/layerzero-definitions';

import { addressParser } from './addressParser';

// Test data generators
const randomBytes = (length: number): Uint8Array => {
    const bytes = new Uint8Array(length);
    for (let i = 0; i < length; i++) {
        bytes[i] = Math.floor(Math.random() * 256);
    }
    return bytes;
};

const randomHex = (byteLength: number): NormalizedHexString => {
    return normalizeHex(bytesToHexPrefixed(randomBytes(byteLength)));
};

const runPropertyTest = <T>(fn: () => T, iterations = 100): void => {
    for (let i = 0; i < iterations; i++) {
        fn();
    }
};

describe('addressParser - hex/EVM chains', () => {
    const evmChains = [ChainName.ETHEREUM, ChainName.AVALANCHE, ChainName.BSC, ChainName.POLYGON];

    evmChains.forEach((chain) => {
        describe(chain, () => {
            const parser = addressParser(chain);

            it('maintains roundtrip invariant: hex -> native -> hex', () => {
                runPropertyTest(() => {
                    const input = randomHex(20);
                    const native = parser.normalizedToNative(input);
                    const back = parser.nativeToNormalized(native);
                    expect(back).toBe(input);
                });
            });

            it('maintains byte representation consistency', () => {
                runPropertyTest(() => {
                    const input = randomHex(20);
                    const nativeStr = parser.normalizedToNativeString(input);
                    const bytesFromHex = hexToBytes(nativeStr);
                    const nativeBytes = parser.nativeToBytes(nativeStr);

                    expect(nativeBytes).toEqual(bytesFromHex);
                });
            });

            it('correctly pads addresses', () => {
                runPropertyTest(() => {
                    const byteLength = Math.floor(Math.random() * 20) + 1; // 1-20 bytes
                    const input = randomHex(byteLength);
                    const native = parser.normalizedToNative(input);
                    expect(native.nativeAddress.length).toBe(42); // Always 20 bytes padded
                    expect(native.chainName).toBe(chain);
                });
            });
        });
    });

    it('normalizedToBytes32Hex pads correctly', () => {
        runPropertyTest(() => {
            const byteLength = Math.floor(Math.random() * 32) + 1; // 1-32 bytes
            const input = randomHex(byteLength);
            const padded = addressParser.normalizedToBytes32Hex(input);
            expect(padded.length).toBe(66); // 32 bytes -> 64 hex chars + 0x
            expect(padded).toBe(hexZeroPad(input as any, 32));
        });
    });

    it('handles edge cases', () => {
        const parser = addressParser(ChainName.ETHEREUM);

        // Empty address
        const empty = normalizeHex('0x');
        expect(parser.normalizedToNative(empty).nativeAddress.length).toBe(42);

        // Maximum value
        const max = normalizeHex(('0x' + 'ff'.repeat(20)) as any);
        expect(parser.nativeToNormalized(parser.normalizedToNative(max))).toBe(max);

        // Single byte
        const single = normalizeHex('0x01');
        expect(parser.normalizedToNative(single).nativeAddress).toBe(hexZeroPad('0x01', 20));
    });
});

describe('addressParser - base58 chains (Solana)', () => {
    const parser = addressParser(ChainName.SOLANA);

    it('maintains roundtrip invariant: hex -> base58 -> hex', () => {
        runPropertyTest(() => {
            const byteLength = Math.floor(Math.random() * 32) + 1; // 1-32 bytes
            const input = randomHex(byteLength);
            const native = parser.normalizedToNative(input);
            const back = parser.nativeToNormalized(native);
            expect(back).toBe(input);
        });
    });

    it('maintains base58 encoding consistency', () => {
        runPropertyTest(() => {
            const input = randomHex(32);
            const nativeStr = parser.normalizedToNativeString(input);
            const expectedBase58 = bs58.encode(hexToBytes(input as any));
            expect(nativeStr).toBe(expectedBase58);

            // Verify bytes consistency
            const nativeBytes = parser.nativeToBytes(nativeStr);
            expect(nativeBytes).toEqual(bs58.decode(expectedBase58));
        });
    });

    it('correctly handles 32-byte padding', () => {
        runPropertyTest(() => {
            const byteLength = Math.floor(Math.random() * 32) + 1;
            const input = randomHex(byteLength);
            const native = parser.normalizedToNativeString(input);
            const padded = parser.nativeToBytes32Hex(native);
            expect(padded.length).toBe(66);

            // Verify padding preserves original value
            expect(parser.nativeToNormalized(native)).toBe(
                normalizeHex(bytesToHexPrefixed(hexToBytes(input as any))),
            );
        });
    });

    it('handles edge cases', () => {
        // Single byte
        const single = normalizeHex('0x01');
        const singleNative = parser.normalizedToNative(single);
        expect(parser.nativeToNormalized(singleNative)).toBe(single);

        // Empty (though typically invalid for Solana)
        const empty = normalizeHex('0x');
        expect(() => parser.normalizedToNative(empty)).not.toThrow();

        // 32 bytes (typical Solana address length)
        const full32 = randomHex(32);
        const native32 = parser.normalizedToNative(full32);
        expect(parser.nativeToNormalized(native32)).toBe(full32);
    });
});

describe('addressParser - chain validation', () => {
    it('validates chain names for nativeToBytes32Hex', () => {
        const evmParser = addressParser(ChainName.ETHEREUM);
        const solanaParser = addressParser(ChainName.SOLANA);

        const evmAddress = evmParser.normalizedToNative(randomHex(20));
        const solanaAddress = solanaParser.normalizedToNative(randomHex(32));

        // nativeTo32BytesHex validates chain names for NativeAddress objects
        expect(() => solanaParser.nativeToBytes32Hex(evmAddress as any)).toThrow(/Cannot convert/);
        expect(() => evmParser.nativeToBytes32Hex(solanaAddress as any)).toThrow(/Cannot convert/);
    });

    it('accepts string addresses without validation', () => {
        const parser = addressParser(ChainName.ETHEREUM);
        const randomAddress = '0x' + 'ab'.repeat(20);

        // String addresses bypass chain validation for all methods
        const result = parser.nativeToNormalized(randomAddress as any);
        expect(result).toBe(normalizeHex(randomAddress as any));

        const padded = parser.nativeToBytes32Hex(randomAddress as any)!;
        expect(padded.length).toBe(66);

        const bytes = parser.nativeToBytes(randomAddress as any);
        expect(bytes).toEqual(hexToBytes(randomAddress));
    });
});

describe('addressParser - TON', () => {
    const parser = addressParser(ChainName.TON);

    it('maintains roundtrip invariant: hex -> ton address -> hex', () => {
        runPropertyTest(() => {
            // TON addresses are typically 32 bytes (256 bits)
            const byteLength = Math.floor(Math.random() * 32) + 1;
            const input = randomHex(byteLength);
            const native = parser.normalizedToNative(input);
            const back = parser.nativeToNormalized(native);

            expect(input).toBe(back);
        });
    });

    it('correctly handles TON address format', () => {
        runPropertyTest(() => {
            const input = randomHex(32);
            const native = parser.normalizedToNative(input);

            // TON addresses should have the correct chain name
            expect(native.chainName).toBe(ChainName.TON);

            // Should be a valid TON address string
            expect(typeof native.nativeAddress).toBe('string');
            expect(native.nativeAddress.length).toBeGreaterThan(0);
        });
    });

    it('maintains byte representation consistency', () => {
        runPropertyTest(() => {
            const input = randomHex(32);
            const nativeStr = parser.normalizedToNativeString(input);
            const nativeBytes = parser.nativeToBytes(nativeStr);

            // Both should produce valid byte arrays
            expect(nativeBytes).toBeInstanceOf(Uint8Array);

            // Converting back should preserve the value
            const backFromBytes = parser.nativeToNormalized(parser.normalizedToNative(input));
            expect(backFromBytes).toBe(input);
        });
    });

    it('correctly handles 32-byte padding', () => {
        runPropertyTest(() => {
            const byteLength = Math.floor(Math.random() * 32) + 1;
            const input = randomHex(byteLength);
            const native = parser.normalizedToNativeString(input);
            const padded = parser.nativeToBytes32Hex(native)!;

            expect(padded.length).toBe(66); // 32 bytes -> 64 hex chars + 0x

            // Verify the numeric value is preserved
            const paddedBigInt = BigInt(padded);
            const inputBigInt = BigInt(input as any);
            expect(paddedBigInt).toBe(inputBigInt);
        });
    });

    it('handles edge cases', () => {
        // Zero address
        const zero = normalizeHex('0x0');
        const zeroNative = parser.normalizedToNative(zero);
        expect(() => parser.nativeToNormalized(zeroNative)).not.toThrow();

        // Large values
        const large = normalizeHex(('0x' + 'ff'.repeat(32)) as any);
        const largeNative = parser.normalizedToNative(large);
        const largeBack = parser.nativeToNormalized(largeNative);
        expect(BigInt(largeBack as any)).toBe(BigInt(large as any));

        // Single byte
        const single = normalizeHex('0x01');
        const singleNative = parser.normalizedToNative(single);
        const singleBack = parser.nativeToNormalized(singleNative);
        expect(BigInt(singleBack as any)).toBe(1n);
    });

    it('validates native addresses', () => {
        const validAddress = parser.normalizedToNative(randomHex(32));
        expect(parser.validateNative(validAddress.nativeAddress)).toBe(true);

        // Invalid addresses should return false
        expect(parser.validateNative('invalid-ton-address')).toBe(false);
    });
});

describe('addressParser - Stellar', () => {
    const parser = addressParser(ChainName.STELLAR);

    // Valid test addresses from SEP-0023 specification
    const validTestAddresses = {
        // Account address (G...)
        account: 'GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ',
        // Contract address (C...)
        contract: 'CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA',
        // Muxed address (M...)
        muxed: 'MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUQ',
        // Liquidity pool address (L...)
        liquidityPool: 'LA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUPJN',
        // Claimable balance address (B...)
        claimableBalance: 'BAAD6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGR4TU',
        // Signed payload (P...)
        signedPayload:
            'PA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAQACAQDAQCQMBYIBEFAWDANBYHRAEISCMKBKFQXDAMRUGY4DUPB6IBZGM',
    };

    describe('roundtrip conversions', () => {
        it('maintains roundtrip invariant for account addresses (G...)', () => {
            const stellarAddress = validTestAddresses.account;
            const normalized = parser.nativeToNormalized(stellarAddress as any);
            const native = parser.normalizedToNative(normalized)!;

            expect(native.nativeAddress).toBe(stellarAddress);
            expect(native.chainName).toBe(ChainName.STELLAR);
        });

        it('maintains roundtrip invariant for contract addresses (C...)', () => {
            const stellarAddress = validTestAddresses.contract;
            const normalized = parser.nativeToNormalized(stellarAddress as any);
            const native = parser.normalizedToNative(normalized)!;

            expect(native.nativeAddress).toBe(stellarAddress);
        });

        it('maintains roundtrip invariant for muxed addresses (M...)', () => {
            const stellarAddress = validTestAddresses.muxed;
            const normalized = parser.nativeToNormalized(stellarAddress as any);
            const native = parser.normalizedToNative(normalized)!;

            expect(native.nativeAddress).toBe(stellarAddress);
        });

        it('maintains roundtrip invariant for liquidity pool addresses (L...)', () => {
            const stellarAddress = validTestAddresses.liquidityPool;
            const normalized = parser.nativeToNormalized(stellarAddress as any);
            const native = parser.normalizedToNative(normalized)!;

            expect(native.nativeAddress).toBe(stellarAddress);
        });

        it('maintains roundtrip invariant for claimable balance addresses (B...)', () => {
            const stellarAddress = validTestAddresses.claimableBalance;
            const normalized = parser.nativeToNormalized(stellarAddress as any);
            const native = parser.normalizedToNative(normalized)!;

            expect(native.nativeAddress).toBe(stellarAddress);
        });

        it('maintains roundtrip invariant for signed payload (P...)', () => {
            const stellarAddress = validTestAddresses.signedPayload;
            const normalized = parser.nativeToNormalized(stellarAddress as any);
            const native = parser.normalizedToNative(normalized)!;

            expect(native.nativeAddress).toBe(stellarAddress);
        });
    });

    describe('validation', () => {
        it('validates all valid Stellar address types', () => {
            expect(parser.validateNative(validTestAddresses.account)).toBe(true);
            expect(parser.validateNative(validTestAddresses.contract)).toBe(true);
            expect(parser.validateNative(validTestAddresses.muxed)).toBe(true);
            expect(parser.validateNative(validTestAddresses.liquidityPool)).toBe(true);
            expect(parser.validateNative(validTestAddresses.claimableBalance)).toBe(true);
            expect(parser.validateNative(validTestAddresses.signedPayload)).toBe(true);
        });

        it('rejects invalid Stellar addresses', () => {
            // Invalid addresses from SEP-0023 test cases
            const invalidAddresses = [
                'GAAAAAAAACGC6', // Invalid length
                'GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZA', // Invalid length
                'G47QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVP2I', // Invalid algorithm
                'invalid-stellar-address',
                '',
                '0x1234567890abcdef', // EVM address
            ];

            invalidAddresses.forEach((addr) => {
                expect(parser.validateNative(addr)).toBe(false);
            });
        });

        it('rejects undefined addresses', () => {
            expect(parser.validateNative(undefined)).toBe(false);
        });
    });

    describe('ASCII encoding/decoding', () => {
        it('converts Stellar address to UTF-8 bytes correctly', () => {
            const stellarAddress = validTestAddresses.account;
            const bytes = parser.nativeToBytes(stellarAddress as any)!;

            // Verify it's UTF-8 encoded ASCII
            const decoder = new TextDecoder();
            const decoded = decoder.decode(bytes);
            expect(decoded).toBe(stellarAddress);
        });

        it('preserves address length in byte representation', () => {
            // Account address (56 chars)
            const accountBytes = parser.nativeToBytes(validTestAddresses.account as any)!;
            expect(accountBytes.length).toBe(56);

            // Muxed address (69 chars)
            const muxedBytes = parser.nativeToBytes(validTestAddresses.muxed as any)!;
            expect(muxedBytes.length).toBe(69);

            // Signed payload (variable length)
            const payloadBytes = parser.nativeToBytes(validTestAddresses.signedPayload as any)!;
            expect(payloadBytes.length).toBeGreaterThan(56);
        });

        it('normalizedToNativeString returns the address string', () => {
            const stellarAddress = validTestAddresses.account;
            const normalized = parser.nativeToNormalized(stellarAddress as any);
            const nativeString = parser.normalizedToNativeString(normalized);

            expect(nativeString).toBe(stellarAddress);
        });
    });

    describe('parseNative', () => {
        it('parses valid Stellar addresses', () => {
            const stellarAddress = validTestAddresses.account;
            const normalized = parser.parseNative(stellarAddress as any);

            // Should return normalized hex representation
            expect(normalized).toMatch(/^0x[0-9a-f]+$/);
        });

        it('throws on invalid addresses', () => {
            expect(() => parser.parseNative('invalid-stellar-address' as any)).toThrow(
                /Cannot convert/,
            );
        });

        it('parseNativeToUnpaddedHex returns hex string', () => {
            const stellarAddress = validTestAddresses.account;
            const hex = parser.parseNativeToUnpaddedHex(stellarAddress as any);

            expect(hex).toMatch(/^0x[0-9a-f]+$/);
            expect(hex).toBe(parser.parseNative(stellarAddress as any));
        });
    });

    describe('bytes32 operations', () => {
        it('decodes account address (G...) to 32 bytes', () => {
            const stellarAddress = validTestAddresses.account;
            const bytes32Hex = parser.nativeToBytes32Hex(stellarAddress as any)!;
            const bytes32 = parser.nativeToBytes32(stellarAddress as any)!;

            // Should be 32 bytes (64 hex chars + 0x)
            expect(bytes32Hex.length).toBe(66);
            expect(bytes32Hex).toMatch(/^0x[0-9a-f]{64}$/);

            // Bytes should be 32 bytes
            expect(bytes32.length).toBe(32);

            // Both methods should produce consistent results
            expect(bytesToHexPrefixed(bytes32)).toBe(bytes32Hex);
        });

        it('decodes contract address (C...) to 32 bytes', () => {
            const stellarAddress = validTestAddresses.contract;
            const bytes32Hex = parser.nativeToBytes32Hex(stellarAddress as any)!;
            const bytes32 = parser.nativeToBytes32(stellarAddress as any)!;

            expect(bytes32Hex.length).toBe(66);
            expect(bytes32.length).toBe(32);
            expect(bytesToHexPrefixed(bytes32)).toBe(bytes32Hex);
        });

        it('decodes liquidity pool address (L...) to 32 bytes', () => {
            const stellarAddress = validTestAddresses.liquidityPool;
            const bytes32Hex = parser.nativeToBytes32Hex(stellarAddress as any)!;
            const bytes32 = parser.nativeToBytes32(stellarAddress as any)!;

            expect(bytes32Hex.length).toBe(66);
            expect(bytes32.length).toBe(32);
            expect(bytesToHexPrefixed(bytes32)).toBe(bytes32Hex);
        });

        it('decodes claimable balance address (B...) to 32 bytes (skipping version byte)', () => {
            const stellarAddress = validTestAddresses.claimableBalance;
            const bytes32Hex = parser.nativeToBytes32Hex(stellarAddress as any)!;
            const bytes32 = parser.nativeToBytes32(stellarAddress as any)!;

            // Should be 32 bytes (64 hex chars + 0x)
            expect(bytes32Hex.length).toBe(66);
            expect(bytes32Hex).toMatch(/^0x[0-9a-f]{64}$/);

            // Bytes should be 32 bytes (the hash, without the version byte)
            expect(bytes32.length).toBe(32);

            // Both methods should produce consistent results
            expect(bytesToHexPrefixed(bytes32)).toBe(bytes32Hex);
        });

        it('throws error for muxed address (M...) - not 32 bytes', () => {
            const stellarAddress = validTestAddresses.muxed;

            expect(() => parser.nativeToBytes32Hex(stellarAddress as any)).toThrow(
                /nativeToBytes32 is not supported for Stellar muxed addresses/,
            );
            expect(() => parser.nativeToBytes32(stellarAddress as any)).toThrow(
                /nativeToBytes32 is not supported for Stellar muxed addresses/,
            );
        });

        it('throws error for signed payload (P...) - variable length', () => {
            const stellarAddress = validTestAddresses.signedPayload;

            expect(() => parser.nativeToBytes32Hex(stellarAddress as any)).toThrow(
                /nativeToBytes32 is not supported for Stellar signed payload addresses/,
            );
            expect(() => parser.nativeToBytes32(stellarAddress as any)).toThrow(
                /nativeToBytes32 is not supported for Stellar signed payload addresses/,
            );
        });

        it('returns undefined for undefined input', () => {
            expect(parser.nativeToBytes32Hex(undefined as any)).toBeUndefined();
            expect(parser.nativeToBytes32(undefined as any)).toBeUndefined();
        });

        it('decodes all supported address types to 32 bytes', () => {
            // Account (G), contract (C), liquidity pool (L), and claimable balance (B) addresses can be converted to 32 bytes
            const accountBytes = parser.nativeToBytes32(validTestAddresses.account as any)!;
            const contractBytes = parser.nativeToBytes32(validTestAddresses.contract as any)!;
            const poolBytes = parser.nativeToBytes32(validTestAddresses.liquidityPool as any)!;
            const balanceBytes = parser.nativeToBytes32(
                validTestAddresses.claimableBalance as any,
            )!;

            // All should be exactly 32 bytes
            expect(accountBytes.length).toBe(32);
            expect(contractBytes.length).toBe(32);
            expect(poolBytes.length).toBe(32);
            expect(balanceBytes.length).toBe(32);

            // Should produce consistent hex conversions
            const accountHex = parser.nativeToBytes32Hex(validTestAddresses.account as any)!;
            const contractHex = parser.nativeToBytes32Hex(validTestAddresses.contract as any)!;
            const poolHex = parser.nativeToBytes32Hex(validTestAddresses.liquidityPool as any)!;
            const balanceHex = parser.nativeToBytes32Hex(
                validTestAddresses.claimableBalance as any,
            )!;

            expect(bytesToHexPrefixed(accountBytes)).toBe(accountHex);
            expect(bytesToHexPrefixed(contractBytes)).toBe(contractHex);
            expect(bytesToHexPrefixed(poolBytes)).toBe(poolHex);
            expect(bytesToHexPrefixed(balanceBytes)).toBe(balanceHex);
        });

        it('handles ChainNativeAddress objects for bytes32 operations', () => {
            const nativeObj = {
                chainName: ChainName.STELLAR,
                nativeAddress: validTestAddresses.account,
            };

            const bytes32Hex = parser.nativeToBytes32Hex(nativeObj as any)!;
            const bytes32 = parser.nativeToBytes32(nativeObj as any)!;

            expect(bytes32Hex.length).toBe(66);
            expect(bytes32.length).toBe(32);
        });
    });

    describe('address type preservation', () => {
        it('preserves address type information through conversion', () => {
            const addresses = [
                { addr: validTestAddresses.account, prefix: 'G' },
                { addr: validTestAddresses.contract, prefix: 'C' },
                { addr: validTestAddresses.muxed, prefix: 'M' },
                { addr: validTestAddresses.liquidityPool, prefix: 'L' },
                { addr: validTestAddresses.claimableBalance, prefix: 'B' },
                { addr: validTestAddresses.signedPayload, prefix: 'P' },
            ];

            addresses.forEach(({ addr, prefix }) => {
                const normalized = parser.nativeToNormalized(addr as any);
                const recovered = parser.normalizedToNative(normalized)!;

                // Should preserve the prefix indicating address type
                expect(recovered.nativeAddress.startsWith(prefix)).toBe(true);
                expect(recovered.nativeAddress).toBe(addr);
            });
        });
    });

    describe('edge cases', () => {
        it('handles ChainNativeAddress objects', () => {
            const nativeObj = {
                chainName: ChainName.STELLAR,
                nativeAddress: validTestAddresses.account,
            };

            const normalized = parser.nativeToNormalized(nativeObj as any);
            expect(normalized).toMatch(/^0x[0-9a-f]+$/);
        });

        it('returns undefined for undefined inputs', () => {
            expect(parser.normalizedToNative(undefined as any)).toBeUndefined();
            expect(parser.normalizedToNativeString(undefined as any)).toBeUndefined();
            expect(parser.nativeToNormalized(undefined as any)).toBeUndefined();
            expect(parser.nativeToBytes(undefined as any)).toBeUndefined();
        });

        it('handles different address lengths correctly', () => {
            // 56-char addresses
            const short = validTestAddresses.account;
            const shortBytes = parser.nativeToBytes(short as any)!;
            expect(shortBytes.length).toBe(56);

            // 69-char addresses (muxed)
            const medium = validTestAddresses.muxed;
            const mediumBytes = parser.nativeToBytes(medium as any)!;
            expect(mediumBytes.length).toBe(69);

            // Variable-length (signed payload)
            const long = validTestAddresses.signedPayload;
            const longBytes = parser.nativeToBytes(long as any)!;
            expect(longBytes.length).toBeGreaterThan(69);
        });
    });

    describe('encoding type', () => {
        it('has STELLAR encoding type', () => {
            expect(parser.encoding).toBe('stellar');
        });
    });
});
