type ArrowFunction<Return = any, Parameters extends any[] = any[]> = (...args: Parameters) => Return

type And<Condition0 extends boolean, Condition1 extends boolean> = Condition0 extends true ? (Condition1 extends true ? true : false) : false
type Not<Condition extends boolean> = Condition extends true ? false : true

type IsAny<Type> = (any extends Type ? true : false) extends true ? true : false
type Is<Type, Expect> = Not<IsAny<Type>> extends true ? (Type extends Expect ? true : false) : false

type Opt<Type, Condition extends boolean> = Condition extends true ? Type : never
type Cast<Type, Condition> = Type extends Condition ? Type : never

type PropertyValue<Type> = {
    [Key in keyof Type as Opt<Key, Not<Is<Type[Key], object | ArrowFunction>>>]: Type[Key]
}
type PropertyObject<Type> = {
    [Key in keyof Type as Opt<Key, And<Is<Type[Key], object>, Not<Is<Type[Key], ArrowFunction>>>>]: Type[Key]
}
type Interface<Type> = {
    [Key in keyof Type as Opt<Key, Is<Type[Key], ArrowFunction>>]: Cast<Type[Key], ArrowFunction>
}

type MockPropertyValue<Type> = PropertyValue<Type> & {
    readonly [Key in 'getMock' as Opt<Key, Is<keyof PropertyValue<Type>, string>>]: {
        readonly [Key in keyof PropertyValue<Type>]: jest.Mock<PropertyValue<Type>[Key], []>
    }
} & {
    readonly [Key in 'setMock' as Opt<Key, Is<keyof PropertyValue<Type>, string>>]: {
        readonly [Key in keyof PropertyValue<Type>]: jest.Mock<void, [PropertyValue<Type>[Key]]>
    }
}
type MockPropertyObject<Type> = {
    readonly [Key in keyof PropertyObject<Type>]: Mock<PropertyObject<Type>[Key]>
}
type MockInterface<Type> = {
    readonly [Key in keyof Interface<Type>]: jest.Mock<ReturnType<Interface<Type>[Key]>, Parameters<Interface<Type>[Key]>>
}

export type Mock<Type> = MockPropertyValue<Type> & MockPropertyObject<Type> & MockInterface<Type>

export const createMock = <Type>(): Mock<Type> => {
    const values: Record<string, any> = {}
    const mocks: Record<string, any> = {}

    const prepareValue = (name: string) => {
        if (!(name in values)) {
            values[name] = {
                value: undefined,
                getMock: jest.fn(() => {
                    return values[name].value
                }),
                setMock: jest.fn((value) => {
                    values[name].value = value
                }),
            }
        }
    }

    const getMock = new Proxy({}, {
        get: (_, name: string) => {
            prepareValue(name)
            return values[name].getMock
        },
        set: () => {
            return false
        },
    })

    const setMock = new Proxy({}, {
        get: (_, name: string) => {
            prepareValue(name)
            return values[name].setMock
        },
        set: () => {
            return false
        },
    })

    return new Proxy(jest.fn(), {
        apply: (call: any, _, argument) => {
            return call(...argument)
        },
        get: (call: any, name: string) => {
            if (name in call || name === 'calls') {
                return call[name]
            }

            if (name in values) {
                return values[name].getMock()
            }

            if (name === 'getMock') {
                return getMock
            }

            if (name === 'setMock') {
                return setMock
            }

            if (!(name in mocks)) {
                mocks[name] = createMock()
            }

            return mocks[name]
        },
        set: (_, name: string, value: any) => {
            prepareValue(name)
            values[name].setMock(value)

            return true
        },
    }) as any as Mock<Type>
}
