use gprimitives::*;
use meta_params::*;
use sails_idl_gen::{program, service};
use sails_idl_meta::{
    AnyServiceMeta, BaseServiceMeta, Identifiable, InterfaceId, MethodMetadata, ProgramMeta,
    ServiceMeta,
};
use sails_type_registry::TypeInfo;
use std::{collections::BTreeMap, result::Result as StdResult};

#[allow(dead_code)]
mod types {
    use super::*;

    /// GenericStruct docs
    #[derive(TypeInfo)]
    pub struct GenericStruct<T> {
        /// GenericStruct field `p1`
        pub p1: T,
    }

    /// GenericConstStruct docs
    #[derive(TypeInfo)]
    pub struct GenericConstStruct<const N: usize> {
        /// GenericStruct field `field`
        field: [u8; N],
    }

    /// GenericEnum docs
    /// with two lines
    #[derive(TypeInfo)]
    pub enum GenericEnum<T1, T2> {
        /// GenericEnum `Variant1` of type 'T1'
        Variant1(T1),
        /// GenericEnum `Variant2` of type 'T2'
        Variant2(T2),
    }

    /// TupleStruct docs
    #[derive(TypeInfo)]
    pub struct TupleStruct(bool);

    #[derive(TypeInfo)]
    pub enum ManyVariants {
        One,
        Two(u32),
        Three(Option<Vec<U256>>),
        Four { a: u32, b: Option<u16> },
        Five(String, Vec<u8>),
        Six((u32,)),
        Seven(GenericEnum<u32, String>),
        Eight([BTreeMap<u32, String>; 10]),
    }

    #[derive(TypeInfo)]
    pub struct DoThatParam {
        pub p1: u32,
        pub p2: String,
        pub p3: ManyVariants,
    }

    #[derive(TypeInfo)]
    pub struct ThatParam {
        pub p1: ManyVariants,
    }
}

#[allow(dead_code)]
mod meta_params {
    use super::{types::*, *};

    #[derive(TypeInfo)]
    pub struct DoThisParams {
        p1: u32,
        p2: String,
        p3: (Option<String>, u8),
        p4: TupleStruct,
        p5: GenericStruct<H256>,
        p6: GenericStruct<String>,
        p7: GenericConstStruct<8>,
        p8: GenericConstStruct<32>,
    }

    #[derive(TypeInfo)]
    pub struct DoThatParams {
        par1: DoThatParam,
    }

    #[derive(TypeInfo)]
    pub struct ThisParams {
        p1: u32,
        p2: String,
        p3: (Option<String>, u8),
        p4: TupleStruct,
        p5: GenericEnum<bool, u32>,
    }

    #[derive(TypeInfo)]
    pub struct ThatParams {
        pr1: ThatParam,
    }

    #[derive(TypeInfo)]
    pub struct SingleParams<T: TypeInfo> {
        pub p1: T,
    }

    #[derive(TypeInfo)]
    pub struct NoParams;
}

#[allow(dead_code)]
#[derive(TypeInfo)]
enum CommandsMeta {
    /// Some description
    DoThis(DoThisParams, String),
    /// Some multiline description
    /// Second line
    /// Third line
    DoThat(DoThatParams, StdResult<(String, u32), (String,)>),
}

#[allow(dead_code)]
#[derive(TypeInfo)]
enum BaseCommandsMeta {
    DoThis(SingleParams<u32>, u32),
    DoThatBase(SingleParams<String>, String),
}

#[allow(dead_code)]
#[derive(TypeInfo)]
enum QueriesMeta {
    /// This is a query
    This(ThisParams, StdResult<(String, u32), String>),
    /// This is a second query
    /// This is a second line
    That(ThatParams, String),
}

#[allow(dead_code)]
#[derive(TypeInfo)]
enum BaseQueriesMeta {
    ThisBase(SingleParams<u16>, u16),
    That(SingleParams<String>, String),
}

#[allow(dead_code)]
#[derive(TypeInfo)]
enum EventsMeta {
    /// `This` Done
    ThisDone(
        /// This is unnamed field, comments ignored
        u32,
    ),
    ThisDoneTwice(
        /// This is the first unnamed field
        u32,
        /// This is the second unnamed field
        u32,
    ),
    /// `That` Done too
    ThatDone {
        /// This is `p1` field
        p1: String,
    },
}

#[allow(dead_code)]
#[derive(TypeInfo)]
enum BaseEventsMeta {
    ThisDoneBase(u32),
    ThatDoneBase { p1: u16 },
}

#[allow(dead_code)]
#[derive(TypeInfo)]
enum AmbiguousBaseEventsMeta {
    ThisDone(u32), // Conflicts with `EventsMeta::ThisDone` even it has the same signatur
    ThatDoneBase { p1: u16 },
}

struct GenericService<C, Q, E, const ID: u64> {
    _commands: std::marker::PhantomData<C>,
    _queries: std::marker::PhantomData<Q>,
    _events: std::marker::PhantomData<E>,
}

impl<C, Q, E, const ID: u64> Identifiable for GenericService<C, Q, E, ID> {
    const INTERFACE_ID: InterfaceId = InterfaceId::from_u64(ID);
}

impl<C, Q, E, const ID: u64> ServiceMeta for GenericService<C, Q, E, ID>
where
    C: TypeInfo,
    Q: TypeInfo,
    E: TypeInfo,
{
    type CommandsMeta = C;
    type QueriesMeta = Q;
    type EventsMeta = E;
    const BASE_SERVICES: &'static [BaseServiceMeta] = &[];
    const METHODS: &'static [MethodMetadata] = &[];
    const ASYNC: bool = false;
}

struct GenericServiceWithBase<C, Q, E, B, const ID: u64> {
    _commands: std::marker::PhantomData<C>,
    _queries: std::marker::PhantomData<Q>,
    _events: std::marker::PhantomData<E>,
    _base: std::marker::PhantomData<B>,
}

impl<C, Q, E, B, const ID: u64> Identifiable for GenericServiceWithBase<C, Q, E, B, ID> {
    const INTERFACE_ID: InterfaceId = InterfaceId::from_u64(ID);
}

impl<C, Q, E, B, const ID: u64> ServiceMeta for GenericServiceWithBase<C, Q, E, B, ID>
where
    C: TypeInfo,
    Q: TypeInfo,
    E: TypeInfo,
    B: ServiceMeta + 'static,
{
    type CommandsMeta = C;
    type QueriesMeta = Q;
    type EventsMeta = E;
    const BASE_SERVICES: &'static [BaseServiceMeta] = &[BaseServiceMeta::new::<B>("B")];
    const METHODS: &'static [MethodMetadata] = &[];
    const ASYNC: bool = false;
}

type TestServiceMeta = GenericService<CommandsMeta, QueriesMeta, EventsMeta, 0xd42ae9a4dc1efdf0>;

#[allow(dead_code)]
#[derive(TypeInfo)]
enum EmptyCtorsMeta {}

struct TestProgramWithEmptyCtorsMeta;

impl ProgramMeta for TestProgramWithEmptyCtorsMeta {
    type ConstructorsMeta = EmptyCtorsMeta;

    const SERVICES: &'static [(&'static str, AnyServiceMeta)] =
        &[("Service", TestServiceMeta::META)];

    const ASYNC: bool = false;
}

#[allow(dead_code)]
#[derive(TypeInfo)]
enum NonEmptyCtorsMeta {
    /// This is New constructor
    New(NoParams),
    /// This is FromStr constructor
    /// with second line
    FromStr(SingleParams<String>),
}

struct TestProgramWithNonEmptyCtorsMeta;

impl ProgramMeta for TestProgramWithNonEmptyCtorsMeta {
    type ConstructorsMeta = NonEmptyCtorsMeta;

    const SERVICES: &'static [(&'static str, AnyServiceMeta)] = &[("Test", TestServiceMeta::META)];

    const ASYNC: bool = false;
}

struct TestProgramWithMultipleServicesMeta;

impl ProgramMeta for TestProgramWithMultipleServicesMeta {
    type ConstructorsMeta = EmptyCtorsMeta;

    const SERVICES: &'static [(&'static str, AnyServiceMeta)] = &[
        ("Service", TestServiceMeta::META),
        ("SomeService", TestServiceMeta::META),
    ];

    const ASYNC: bool = false;
}

#[test]
fn program_idl_works_with_empty_ctors() {
    let mut idl = String::new();
    program::generate_idl::<TestProgramWithEmptyCtorsMeta>(
        Some("TestProgramWithEmptyCtorsMeta"),
        &mut idl,
    )
    .unwrap();

    insta::assert_snapshot!(idl);

    let generated_idl_program = sails_idl_parser_v2::parse_idl(&idl).unwrap();
    assert!(generated_idl_program.program.is_some());
    let program = generated_idl_program.program.unwrap();
    assert!(program.ctors.is_empty());
    assert_eq!(generated_idl_program.services.len(), 1);
    assert_eq!(generated_idl_program.services[0].funcs.len(), 4);
    assert_eq!(generated_idl_program.services[0].types.len(), 8);
}

#[test]
fn program_idl_works_with_non_empty_ctors() {
    let mut idl = String::new();
    program::generate_idl::<TestProgramWithNonEmptyCtorsMeta>(
        Some("TestProgramWithNonEmptyCtorsMeta"),
        &mut idl,
    )
    .unwrap();

    insta::assert_snapshot!(idl);

    let generated_idl_program = sails_idl_parser_v2::parse_idl(&idl).unwrap();
    assert!(generated_idl_program.program.is_some());
    let program = generated_idl_program.program.unwrap();
    assert_eq!(program.ctors.len(), 2);
    assert_eq!(generated_idl_program.services.len(), 1);
    assert_eq!(generated_idl_program.services[0].funcs.len(), 4);
    assert_eq!(generated_idl_program.services[0].types.len(), 8);
}

#[test]
fn program_idl_works_with_multiple_services() {
    let mut idl = String::new();
    program::generate_idl::<TestProgramWithMultipleServicesMeta>(
        Some("TestProgramWithMultipleServicesMeta"),
        &mut idl,
    )
    .unwrap();

    insta::assert_snapshot!(idl);

    let generated_idl_program = sails_idl_parser_v2::parse_idl(&idl).unwrap();
    assert!(generated_idl_program.program.is_some());
    let program = generated_idl_program.program.unwrap();
    assert!(program.ctors.is_empty());
    assert_eq!(generated_idl_program.services.len(), 1);
    assert_eq!(generated_idl_program.services[0].name.name, "Service");
    assert_eq!(generated_idl_program.services[0].funcs.len(), 4);
    assert_eq!(generated_idl_program.services[0].types.len(), 8);
}

#[test]
fn service_idl_works_with_basics() {
    let mut idl = String::new();
    service::generate_idl::<TestServiceMeta>("TestServiceMeta", &mut idl).unwrap();

    insta::assert_snapshot!(idl);

    let generated_idl_program = sails_idl_parser_v2::parse_idl(&idl).unwrap();
    assert!(generated_idl_program.program.is_none());
    assert_eq!(generated_idl_program.services.len(), 1);
    assert_eq!(generated_idl_program.services[0].funcs.len(), 4);
    assert_eq!(generated_idl_program.services[0].types.len(), 8);
}

#[test]
fn service_idl_works_with_base_services() {
    let mut idl = String::new();
    service::generate_idl::<
        GenericServiceWithBase<
            CommandsMeta,
            QueriesMeta,
            EventsMeta,
            GenericService<BaseCommandsMeta, BaseQueriesMeta, BaseEventsMeta, 0x256fb43427bef08e>,
            0x10a0a7803b912783,
        >,
    >("ServiceMetaWithBase", &mut idl)
    .unwrap();

    insta::assert_snapshot!(idl);

    let generated_idl_program = sails_idl_parser_v2::parse_idl(&idl).unwrap();
    assert!(generated_idl_program.program.is_none());
    assert_eq!(generated_idl_program.services.len(), 2);
    assert_eq!(generated_idl_program.services[0].funcs.len(), 4);
    assert_eq!(generated_idl_program.services[0].types.len(), 0);
    assert_eq!(generated_idl_program.services[0].events.len(), 2);

    assert_eq!(generated_idl_program.services[1].funcs.len(), 4);
    assert_eq!(generated_idl_program.services[1].types.len(), 8);
}
