import { Injector } from "../src/backend/Injector/Injector";
import { FrontworkAdmin } from "../src/backend/Admin/Admin";
import { T1, T2, Tiers } from "../src/backend/Types/Items";

import { RPCSocket } from "rpclibrary";
import { FrontcraftIfc, Auth, User, FrontcraftFeatureIfc, Raid, Character, Rank, Class, Race, SRPriority, Spec, Signup } from "../src/backend/Types/Types";
import { SpecT } from "../src/backend/Types/PlayerSpecs";


type protoAccount<C extends Class = Class> = {
    name: string,
    pwHash?: string
    rank: Rank,
    race: Race,
    class: C,
    spec: SpecT[C]
}

const trialsAndUp = {
    ADMIN: true,
    Guildmaster: true,
    Officer: true,
    Classleader: true,
    Raider: true,
    Trial: true,
    Social: false,
    Guest: false
}

const adminsOnly = {
    ADMIN: true,
    Guildmaster: true,
    Officer: true,
    Classleader: false,
    Raider: false,
    Trial: false,
    Social: false,
    Guest: false
}

const defaultPermissions = [
    {
        rpcname: 'signup', ...trialsAndUp
    }, {
        rpcname: 'reset', ...adminsOnly
    }, {
        rpcname: 'modifyPermissions', ...adminsOnly
    }, {
        rpcname: 'manageGuild', ...adminsOnly
    }, {
        rpcname: 'managePriorities', ...adminsOnly
    }, {
        rpcname: 'softreserveCurrency', ...adminsOnly
    }, {
        rpcname: 'manageRaid', ...adminsOnly
    }]

const testAccounts: protoAccount[] = [
    {
        name: 'Rain',
        race: 'Human',
        class: 'Warrior',
        spec: 'Protection',
        rank: 'Guildmaster'
    }, {
        name: 'Celinda',
        class: 'Warrior',
        race: 'Night Elf',
        spec: 'Protection',
        rank: 'Officer'
    }, {
        name: 'Silver',
        class: 'Druid',
        race: 'Night Elf',
        spec: 'Restoration',
        rank: 'Raider'
    }, {
        name: 'Dagger',
        race: 'Dwarf',
        class: 'Rogue',
        spec: 'Assassination',
        rank: 'Classleader'
    }, {
        name: 'Hope',
        class: 'Paladin',
        race: 'Human',
        spec: 'Holy',
        rank: 'Classleader'
    }, {
        name: 'Shrekd',
        class: 'Warrior',
        race: 'Dwarf',
        spec: 'Fury',
        rank: 'Classleader'
    }, {
        name: 'Teeniweeni',
        class: 'Warlock',
        race: 'Gnome',
        spec: 'Demonology',
        rank: 'Classleader'
    }, {
        name: 'Hagibaba',
        class: 'Priest',
        race: 'Human',
        spec: 'Discipline',
        rank: 'Classleader'
    }, {
        name: 'Muffinbreak',
        class: 'Mage',
        race: 'Gnome',
        spec: 'Arcane',
        rank: 'Classleader'
    },
]


describe('Frontcraft', () => {
    let auth: Auth,
        adminUser: User,
        server: FrontworkAdmin,
        client: RPCSocket & FrontcraftIfc,
        adminClient: RPCSocket & FrontcraftFeatureIfc,
        raids: Raid[] = [],
        users: { [username in string]: { account: User, character: Character, auth: Auth, signup?: Signup, item?: string } } = {}

    const createAccount = (user: User) => {
        return client.UserManager.createUser(user)
    }

    const createAccountAndUser = async (acc: protoAccount) => {
        const account = await createAccount({
            pwhash: 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', //sha256("a")
            rank: acc.rank,
            username: acc.name,
        })
        const auth = await client.UserManager.login(acc.name, 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb')
        const id = await client.CharacterManager.getSpecId(acc.class, acc.spec)
        const char = await client.CharacterManager.createCharacter(auth.token.value, {
            charactername: acc.name,
            specid: id,
            userid: account.id!,
            race: acc.race
        })
        return {
            account: account,
            character: char,
            auth: auth
        }
    }

    before(function (done) {
        this.timeout(10000);

        server = Injector.resolve<FrontworkAdmin>(FrontworkAdmin)
        server.start().then((_server) => {

            RPCSocket.makeSocket<FrontcraftIfc>(20000, 'localhost').then(_client => {
                client = _client


                createAccount({
                    pwhash: 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', //hash("a")
                    rank: 'ADMIN',
                    username: 'a',
                    MC: 2
                }).then(adminUser => {

                    client.UserManager.login(adminUser.username, 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb').then(auth => {
                        const sock = new RPCSocket(
                            auth.port,
                            "localhost"
                        )

                        sock.connect<any>(auth.token.value).then(cli => {
                            adminClient = cli
                            sock.hook('kick', () => {
                                console.log("I got kicked");
                            })
                            sock.hook('getUserData', () => auth)
                            sock.hook('navigate', (where: string) => {
                                console.log("Nagivate client to " + where);
                            })
                            sock.on('error', (e) => {
                                console.log('Socket error', e)
                            })
                            done()
                        })
                    })
                })
            })
        }).catch(done)
    })

    after(() => {
        client.destroy()
        adminClient.destroy()
        server.stop()
    })

    it('create raids', (done) => {
        let insertRaid = <Raid>{
            description: "Test raid 1",
            title: 'BWL :D',
            start: Date.now().toString(),
            tier: 'BWL'
        }

        adminClient.manageRaid.createRaid(insertRaid).then(() => {
            client.RaidManager.getRaids().then((r) => {
                if (r[0].title === insertRaid.title
                    && r[0].description === insertRaid.description
                    && r[0].tier === "BWL") {
                    raids.push(r[0])
                    done()
                }
            }).catch(done)
        }).catch(done)
    })

    it('create users', (done) => {
        Promise.all(testAccounts.map(acc => createAccountAndUser(acc))).then(accs => {
            if (accs.length === testAccounts.length) {
                accs.forEach(acc => {
                    users[acc.account.username] = acc
                })
                done()
            }
        }).catch(done)
    })

    it('should sign up', (done) => {
        Promise.all(Object.values(users).map((user) =>
            adminClient.signup.sign(user.auth.token.value, user.character, raids[0], false).catch(done)
        )).then(x => {
            adminClient.signup.getSignups(raids[0]).then(s => {
                if (s.length == testAccounts.length) {
                    s.forEach(sign => {
                        users[sign.username].signup = sign
                    })
                    done()
                } else {
                    done("Unexpected number of signups: " + s.length)
                }
            })
        })
    })

    it('calculate priorities', (done) => {
        const makePrio = async (itemname: string, spec?: Spec, race?: Race, mod: number = 0, description: string = "") => {
            let specid
            if (spec)
                specid = await client.CharacterManager.getSpecId(spec.class, <any>spec.specname)

            await adminClient.managePriorities.setPriority(itemname, {
                specid: specid,
                race: race,
                modifier: mod,
                description: description
            })
        }


        Promise.all([
            makePrio(
                'Bracers of Arcane Accuracy',
                { class: 'Warlock', specname: 'Demonology' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Bracers of Arcane Accuracy',
                { class: 'Warlock', specname: 'Affliction' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Bracers of Arcane Accuracy',
                { class: 'Warlock', specname: 'Destruction' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Bracers of Arcane Accuracy',
                { class: 'Mage', specname: 'Arcane' },
                undefined, 1, "hit bias"
            ),
            makePrio(
                'Bracers of Arcane Accuracy',
                { class: 'Mage', specname: 'Frost' },
                undefined, 1, "hit bias"
            ),
            makePrio(
                'Bracers of Arcane Accuracy',
                { class: 'Mage', specname: 'Fire' },
                undefined, 1, "hit bias"
            ),

            makePrio(
                'Maladath, Runed Blade of the Black Flight',
                undefined,
                "Human", -2, "[1] Non-Human"
            ),
            makePrio(
                'Maladath, Runed Blade of the Black Flight',
                { class: 'Rogue', specname: 'Combat' },
                undefined, 2, "Weapon skill bias"
            ),
            makePrio(
                'Maladath, Runed Blade of the Black Flight',
                { class: 'Warrior', specname: 'Fury' },
                'Human', 4, "+2 Fury Warrior (weapon skill bias), +2 to offset [1]"
            ),
            makePrio(
                'Maladath, Runed Blade of the Black Flight',
                { class: 'Warrior', specname: 'Protection' },
                'Human', 5, "+3 Prot Warrior, +2 to offset [1]"
            ),

            makePrio(
                'Cloak of Firemaw',
                { class: 'Rogue', specname: 'Assassination' },
                undefined, 2, "agi-to-ap bias"
            ),
            makePrio(
                'Cloak of Firemaw',
                { class: 'Rogue', specname: 'Combat' },
                undefined, 2, "agi-to-ap bias"
            ),
            makePrio(
                'Cloak of Firemaw',
                { class: 'Rogue', specname: 'Subtlety' },
                undefined, 2, "agi-to-ap bias"
            ),

            makePrio(
                'Band of Forced Concentration',
                { class: 'Warlock', specname: 'Demonology' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Band of Forced Concentration',
                { class: 'Warlock', specname: 'Affliction' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Band of Forced Concentration',
                { class: 'Warlock', specname: 'Destruction' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Band of Forced Concentration',
                { class: 'Mage', specname: 'Arcane' },
                undefined, 1, "hit bias"
            ),
            makePrio(
                'Band of Forced Concentration',
                { class: 'Mage', specname: 'Frost' },
                undefined, 1, "hit bias"
            ),
            makePrio(
                'Band of Forced Concentration',
                { class: 'Mage', specname: 'Fire' },
                undefined, 1, "hit bias"
            ),

            makePrio(
                'Chromatic Boots',
                { class: 'Warrior', specname: 'Protection' },
                undefined, 2, "hit bias"
            ),

            makePrio(
                'Crul\'shorukh, Edge of Chaos',
                undefined,
                "Human", -2, "Non-human"
            ),
            makePrio(
                'Crul\'shorukh, Edge of Chaos',
                { class: 'Warrior', specname: 'Protection' },
                undefined, 2, "nice dps bias"
            ),
            makePrio(
                'Crul\'shorukh, Edge of Chaos',
                { class: 'Warrior', specname: 'Fury' },
                undefined, 2, "nice dps bias"
            ),

            makePrio(
                'Drake Talon Pauldrons',
                { class: 'Warrior', specname:'Protection'},
                undefined, 2, 'dodge + stats-to-threat bias'
            ),
            
            makePrio(
                'Helm of Endless Rage',
                { class: 'Warrior', specname:'Protection'},
                undefined, 2, 'stats-to-threat bias'
            ),

            makePrio(
                'Drake Fang Talisman',
                { class: 'Warrior', specname: 'Protection' },
                undefined, 5, "hit bias"
            ),
            makePrio(
                'Drake Fang Talisman',
                { class: 'Rogue', specname: 'Assassination' },
                undefined, 4, "hit bias"
            ),
            makePrio(
                'Drake Fang Talisman',
                { class: 'Rogue', specname: 'Combat' },
                undefined, 4, "hit bias"
            ),
            makePrio(
                'Drake Fang Talisman',
                { class: 'Rogue', specname: 'Subtlety' },
                undefined, 4, "hit bias"
            ),
            makePrio(
                'Drake Fang Talisman',
                { class: 'Warrior', specname: 'Fury' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Drake Fang Talisman',
                { class: 'Druid', specname: 'Feral (DPS)' },
                undefined, 2, "hit bias"
            ),

            makePrio(
                'Circle of Applied Force',
                { class: 'Warrior', specname: 'Fury' },
                undefined, 2, "str-to-ap bias"
            ),
            makePrio(
                'Circle of Applied Force',
                { class: 'Druid', specname: 'Feral (DPS)' },
                undefined, 3, "str-to-ap bias + agi-to-ap bias"
            ),

            makePrio(
                'Empowered Leggings',
                { class: 'Paladin', specname: 'Holy' },
                undefined, 2, "crit bias"
            ),
            makePrio(
                'Empowered Leggings',
                { class: 'Druid', specname: 'Restoration' },
                undefined, 2, "crit bias"
            ),

            makePrio(
                'Boots of the Shadow Flame',
                { class: 'Rogue', specname: 'Assassination' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Boots of the Shadow Flame',
                { class: 'Rogue', specname: 'Combat' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Boots of the Shadow Flame',
                { class: 'Rogue', specname: 'Subtlety' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Boots of the Shadow Flame',
                { class: 'Druid', specname: 'Feral (DPS)' },
                undefined, 2, "hit bias"
            ),

            makePrio(
                'Neltharion\'s Tear',
                { class: 'Warlock', specname: 'Demonology' },
                undefined, 4, "hit bias"
            ),
            makePrio(
                'Neltharion\'s Tear',
                { class: 'Warlock', specname: 'Affliction' },
                undefined, 4, "hit bias"
            ),
            makePrio(
                'Neltharion\'s Tear',
                { class: 'Warlock', specname: 'Destruction' },
                undefined, 4, "hit bias"
            ),
            makePrio(
                'Neltharion\'s Tear',
                { class: 'Mage', specname: 'Arcane' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Neltharion\'s Tear',
                { class: 'Mage', specname: 'Frost' },
                undefined, 2, "hit bias"
            ),
            makePrio(
                'Neltharion\'s Tear',
                { class: 'Mage', specname: 'Fire' },
                undefined, 2, "hit bias"
            ),

            makePrio(
                'Cloak of Draconic Might',
                { class: 'Warrior', specname: 'Fury' },
                undefined, 2, "str-to-ap bias"
            ),
            makePrio(
                'Cloak of Draconic Might',
                { class: 'Druid', specname: 'Feral (DPS)' },
                undefined, 1, "str-to-ap bias"
            ),
        ]).then(() => {
            const user = Object.values(users)[0]
            client.ItemManager.calculatePriorities("Maladath, Runed Blade of the Black Flight", user.character).then(sum => {
                if (sum === 3)
                    done()
                else
                    console.log("Expected prio on maladath to be 4, but was:", sum, user.character);
            })

        })
    })

    it('buy token', (done) => {
        Promise.all(Object.values(users).map(async (user) => {
            const itemname = "Maladath, Runed Blade of the Black Flight" //T1[Math.floor(T1.length*Math.random())]
            const modifier = await client.ItemManager.calculatePriorities(itemname, user.character)

            const token = await client.ItemManager.buyToken(user.auth.token.value, user.character.charactername, itemname, user.signup!)
            users[user.account.username].item = itemname

            if (!token) return false
            return modifier + 1 === token.level
        })).then(success => {
            if (success.reduce((prev, curr) => prev && curr, true)) done()
        })
    })

    it('not buy token without currency', (done) => {
        const user = Object.values(users)[0]
        const itemname = "Maladath, Runed Blade of the Black Flight"
        client.ItemManager.buyToken(user.auth.token.value, user.character.charactername, itemname, user.signup!).then(token => {
            if (!token)
                done()
            else {
                console.log("Unexpected token", token);
            }
        })
    })

    it('upgrade token', (done) => {
        const user = Object.values(users)[0]
        const itemname = "Maladath, Runed Blade of the Black Flight"
        client.ItemManager.getItem(itemname).then(async item => {
            await adminClient.softreserveCurrency.incrementCurrency(user.account, item.tier, 2)
            const before = await client.ItemManager.getToken(user.character, item)

            if (!before || before.level !== 4) {
                console.log("expected level to be 4", before ? before.level : '?');
                return
            }

            await client.ItemManager.buyToken(user.auth.token.value, user.character.charactername, itemname, user.signup!)
            const after = await client.ItemManager.buyToken(user.auth.token.value, user.character.charactername, itemname, user.signup!)

            if (!after || after.level !== 6) {
                console.log("expected level after to be 6", after ? after.level : '?');
                return
            }
            done()
        })
    })

    it('should buy more tokens', (done) => {
        Promise.all(Object.values(users).map(async (user) => {
            await adminClient.softreserveCurrency.incrementCurrency(user.account, raids[0].tier, 1)
            return await client.ItemManager
                .buyToken(
                    user.auth.token.value,
                    user.character.charactername,
                    T2[Math.floor(T2.length * Math.random())],
                    user.signup!
                )
        })).then(_ => {
            done()
        })
    })

    it('can set permissions', (done) => {
        Promise.all(
            defaultPermissions.map(perm => adminClient.modifyPermissions.setPermission(perm)),
        ).then(_ => {
            done()
        })
    })

    it('start raid', (done) => {
        client.RaidManager.getRaids().then((r) => {
            adminClient.manageRaid.startRaid(raids[0]).then(async data => {
                const dbRaids = await client.RaidManager.getRaids()
                if (dbRaids.length === 0) {
                    await client.UserManager.getUser(testAccounts[0].name).then(dbUser => {
                        if (dbUser && dbUser.BWL === 1) {
                            adminClient.signup.getSignups(raids[0]).then(signups => {
                                if (signups.length === 0) {
                                    done()
                                } else {
                                    console.log(signups);
                                }
                            })
                        }
                        else {
                            console.log("Bad user currency", dbUser);
                        }
                    })
                }
            })
        })
    })

    it('reset system', (done) => {
        adminClient.reset.wipeCurrencyAndItems().then(() => {
            client.UserManager.getUser(testAccounts[0].name).then(user => {
                if (user && user.MC === 1) {
                    client.ItemManager.getTokens(users[testAccounts[0].name.toLowerCase()].character, ['BWL']).then(tokens => {
                        if (tokens!.length === 0) {
                            done()
                        } else {
                            console.log(tokens);
                        }
                    })
                } else {
                    console.log(user)
                }
            })
        })
    })

    it('implements loot system correctly', (done) => {
        const ONE_WEEK = 4800000000

        const Raid = (week: number, tier: string) => {
            return <Raid>{
                description: tier + " Test raid 1",
                title: tier,
                start: (week * ONE_WEEK + Date.now()).toString(),
                tier: tier
            }
        }
        const user = Object.values(users)[0]
        const createRaid = adminClient.manageRaid.createRaid
        const sign = async (raid: Raid, late = false) => await adminClient.signup.sign(user.auth.token.value, user.character, raid, late)
        const buyToken = async (signup: Signup, itemname: string) => await client.ItemManager.buyToken(user.auth.token.value, user.character.charactername, itemname, signup)
        const getCurrency = async (tier: Tiers) => {
            const dbUser = await client.UserManager.getUser(user.account.username)
            return dbUser[tier]
        }

        createRaid(Raid(0, 'BWL')).then(async (BWL0: Raid) => {
            const T2_0 = await client.ItemManager.getItem(T2[0])
            //const BWL0 = await createRaid(Raid(0, 'BWL'))
            const signupBWL0 = await sign(BWL0)
            let BWL = await getCurrency(BWL0.tier)
            let token = await buyToken(signupBWL0, T2[0])
            const BWLAfter = await getCurrency(BWL0.tier)

            if (!token
                || BWL - BWLAfter != 1) {
                console.log("Bad Token status", BWL0, signupBWL0, BWL, BWLAfter)
                done(new Error("Bad Token status 0"))
                return
            }

            let reserves = await client.ItemManager.getTokens(user.character, [BWL0.tier], true)
            let streaks = await client.ItemManager.getTokens(user.character, [BWL0.tier], false)
            if (reserves!.length != 1
                || streaks!.length != 0
                || reserves![0].itemname !== T2_0.itemname
                || reserves![0].level !== 1) {
                console.log("Bad Token status 1", reserves, streaks);
                done(new Error("Bad Token status"))
                return
            }
            await adminClient.manageRaid.startRaid(BWL0)
            reserves = await client.ItemManager.getTokens(user.character, [BWL0.tier], true)
            streaks = await client.ItemManager.getTokens(user.character, [BWL0.tier], false)
            if (reserves!.length != 0
                || streaks!.length != 1
                || streaks![0].itemname !== T2[0]
                || streaks![0].level !== 1) {
                console.log("Bad Token status", reserves, streaks);
                done(new Error("Bad Token status 2"))
                return
            }

            const BWL1 = await createRaid(Raid(1, 'BWL'))

            let signupBWL1 = await sign(BWL1)
            BWL = await getCurrency(BWL1.tier)
            await adminClient.manageRaid.adminUnsign(user.character, BWL1)
            let afterUnsign = await getCurrency(BWL1.tier)
            if (BWL !== afterUnsign) {
                console.log("Expected currency to be equal", BWL, afterUnsign)
                done(new Error("Expected currency to be equal"))
                return
            }

            signupBWL1 = await sign(BWL1)
            BWL = await getCurrency(BWL1.tier)
            await buyToken(signupBWL1, T2[1])
            await adminClient.manageRaid.adminUnsign(user.character, BWL1)
            afterUnsign = await getCurrency(BWL1.tier)
            if (BWL !== afterUnsign) {
                console.log("Expected currency to be equal", BWL, afterUnsign)
                done(new Error("Expected currency to be equal"))
                return
            }

            signupBWL1 = await sign(BWL1)
            await buyToken(signupBWL1, T2[1])
            reserves = await client.ItemManager.getTokens(user.character, [BWL1.tier], true)
            streaks = await client.ItemManager.getTokens(user.character, [BWL1.tier], false)
            if (reserves!.length != 1
                || streaks!.length != 0
                || reserves![0].itemname !== T2[1]
                || reserves![0].level !== 1) {
                console.log("Bad Token status", reserves, streaks);
                done(new Error("Bad Token status 3"))
                return
            }

            BWL = await getCurrency(BWL1.tier)
            const data = await adminClient.manageRaid.startRaid(BWL1)

            const afterStart = await getCurrency(BWL1.tier)
            if (BWL != 0 || afterStart != 1) {
                console.log("Wrong currency values", BWL, afterStart)
                done(new Error("Wrong currency values"))
                return
            }

            done()

        }).catch(e => {
            console.log(e);

            done(e)
        })
    })
})