use std::any::TypeId;
use std::mem::type_info::{Abi, FnPtr, Type, TypeKind};

const STRING_TY: TypeId = const { TypeId::of::<String>() };
const U8_TY: TypeId = const { TypeId::of::<u8>() };
const _U8_REF_TY: TypeId = const { TypeId::of::<&u8>() };
const UNIT_TY: TypeId = const { TypeId::of::<()>() };
const TUPLE_STRING_U8_TY: TypeId = const { TypeId::of::<(String, u8)>() };

#[test]
fn test_fn_ptrs() {
    let TypeKind::FnPtr(FnPtr {
        unsafety: false,
        abi: Abi::ExternRust,
        inputs: &[],
        output,
        variadic: false,
        is_splatted: false,
        splatted_index: _,
    }) = (const { Type::of::<fn()>().kind })
    else {
        panic!();
    };
    assert_eq!(output, UNIT_TY);
}
#[test]
fn test_ref() {
    const {
        // references are tricky because the lifetimes give the references different type ids
        // so we check the pointees instead
        let TypeKind::FnPtr(FnPtr {
            unsafety: false,
            abi: Abi::ExternRust,
            inputs: &[ty1, ty2],
            output,
            variadic: false,
            is_splatted: false,
            splatted_index: _,
        }) = (const { Type::of::<fn(&u8, &u8)>().kind })
        else {
            panic!();
        };
        if output != UNIT_TY {
            panic!();
        }
        let TypeKind::Reference(reference) = ty1.info().kind else {
            panic!();
        };
        if reference.pointee != U8_TY {
            panic!();
        }
        let TypeKind::Reference(reference) = ty2.info().kind else {
            panic!();
        };
        if reference.pointee != U8_TY {
            panic!();
        }
    }
}

#[test]
fn test_unsafe() {
    let TypeKind::FnPtr(FnPtr {
        unsafety: true,
        abi: Abi::ExternRust,
        inputs: &[],
        output,
        variadic: false,
        is_splatted: false,
        splatted_index: _,
    }) = (const { Type::of::<unsafe fn()>().kind })
    else {
        panic!();
    };
    assert_eq!(output, UNIT_TY);
}
#[test]
fn test_abi() {
    let TypeKind::FnPtr(FnPtr {
        unsafety: false,
        abi: Abi::ExternRust,
        inputs: &[],
        output,
        variadic: false,
        is_splatted: false,
        splatted_index: _,
    }) = (const { Type::of::<extern "Rust" fn()>().kind })
    else {
        panic!();
    };
    assert_eq!(output, UNIT_TY);

    let TypeKind::FnPtr(FnPtr {
        unsafety: false,
        abi: Abi::ExternC,
        inputs: &[],
        output,
        variadic: false,
        is_splatted: false,
        splatted_index: _,
    }) = (const { Type::of::<extern "C" fn()>().kind })
    else {
        panic!();
    };
    assert_eq!(output, UNIT_TY);

    let TypeKind::FnPtr(FnPtr {
        unsafety: true,
        abi: Abi::Named("system"),
        inputs: &[],
        output,
        variadic: false,
        is_splatted: false,
        splatted_index: _,
    }) = (const { Type::of::<unsafe extern "system" fn()>().kind })
    else {
        panic!();
    };
    assert_eq!(output, UNIT_TY);
}

#[test]
fn test_inputs() {
    let TypeKind::FnPtr(FnPtr {
        unsafety: false,
        abi: Abi::ExternRust,
        inputs: &[ty1, ty2],
        output,
        variadic: false,
        is_splatted: false,
        splatted_index: _,
    }) = (const { Type::of::<fn(String, u8)>().kind })
    else {
        panic!();
    };
    assert_eq!(output, UNIT_TY);
    assert_eq!(ty1, STRING_TY);
    assert_eq!(ty2, U8_TY);

    let TypeKind::FnPtr(FnPtr {
        unsafety: false,
        abi: Abi::ExternRust,
        inputs: &[ty1, ty2],
        output,
        variadic: false,
        is_splatted: false,
        splatted_index: _,
    }) = (const { Type::of::<fn(val: String, p2: u8)>().kind })
    else {
        panic!();
    };
    assert_eq!(output, UNIT_TY);
    assert_eq!(ty1, STRING_TY);
    assert_eq!(ty2, U8_TY);
}

#[test]
fn test_output() {
    let TypeKind::FnPtr(FnPtr {
        unsafety: false,
        abi: Abi::ExternRust,
        inputs: &[],
        output,
        variadic: false,
        is_splatted: false,
        splatted_index: _,
    }) = (const { Type::of::<fn() -> u8>().kind })
    else {
        panic!();
    };
    assert_eq!(output, U8_TY);
}

#[test]
fn test_variadic() {
    let TypeKind::FnPtr(FnPtr {
        unsafety: false,
        abi: Abi::ExternC,
        inputs: [ty1],
        output,
        variadic: true,
        is_splatted: false,
        splatted_index: _,
    }) = &(const { Type::of::<extern "C" fn(u8, ...)>().kind })
    else {
        panic!();
    };
    assert_eq!(output, &UNIT_TY);
    assert_eq!(*ty1, U8_TY);
}

#[test]
fn test_splat() {
    #[rustfmt::skip]
    let TypeKind::FnPtr(fn_ptr_ty) = &(const { Type::of::<fn(#[rustc_splat] (String, u8))>().kind }) else {
        panic!();
    };
    let FnPtr {
        unsafety: false,
        abi: Abi::ExternRust,
        inputs: [ty1],
        output,
        variadic: false,
        is_splatted: true,
        splatted_index: 0,
    } = fn_ptr_ty
    else {
        panic!();
    };
    assert_eq!(output, &UNIT_TY);
    assert_eq!(*ty1, TUPLE_STRING_U8_TY);
    assert_eq!(fn_ptr_ty.splatted(), Some(0));
}

#[test]
fn test_not_splat() {
    let TypeKind::FnPtr(fn_ptr_ty) = &(const { Type::of::<fn((String, u8))>().kind }) else {
        panic!();
    };
    let FnPtr {
        unsafety: false,
        abi: Abi::ExternRust,
        inputs: [ty1],
        output,
        variadic: false,
        is_splatted: false,
        splatted_index: _,
    } = fn_ptr_ty
    else {
        panic!();
    };
    assert_eq!(output, &UNIT_TY);
    assert_eq!(*ty1, TUPLE_STRING_U8_TY);
    assert_eq!(fn_ptr_ty.splatted(), None);
}
