/**
 * Miscellaneous Random Battle-related tests not associated with a particular generation.
 */

'use strict';

const assert = require('../assert');
const common = require('../common');
const { Utils } = require('../../dist/lib');
const { testTeam, assertSetValidity, validateLearnset } = require('./tools');
const { default: Dex } = require('../../dist/sim/dex');

describe('value rule support (slow)', () => {
	it('should generate teams of the proper length for the format (i.e. support Max Team Size)', () => {
		testTeam({ format: 'gen9randombattle', rounds: 100 }, team => assert.equal(team.length, 6));
		testTeam({ format: 'gen9challengecup1v1', rounds: 100 }, team => assert.equal(team.length, 6));
		testTeam({ format: 'gen9hackmonscup', rounds: 100 }, team => assert.equal(team.length, 6));

		testTeam({ format: 'gen8multirandombattle', rounds: 100 }, team => assert.equal(team.length, 3));
		testTeam({ format: 'gen8cap1v1', rounds: 100 }, team => assert.equal(team.length, 3));

		testTeam({ format: 'gen7randombattle', rounds: 100 }, team => assert.equal(team.length, 6));
	});

	for (let gen = 1; gen <= 9; gen++) {
		const formatID = `gen${gen}randombattle`;
		const dex = Dex.forFormat(formatID);
		for (const count of [1, 3, 24]) {
			// This is tough to test in Gen 1 because we don't know how many moves a Pokémon ought to have,
			// so we only test 'Max Move Count = 1' in Gen 1.
			if (gen === 1 && count !== 1) continue;
			const format = Dex.formats.get(`${formatID}@@@Max Move Count = ${count}`);
			// New set format
			if (gen >= 2) {
				// Due to frontloading of moveset generation, formats with the new set format do not support
				// Max Move Counts less than 4
				if (count < 4) continue;
				const setsJSON = require(`../../dist/data/random-battles/gen${gen}/sets.json`);

				it(`${format.name} should support Max Move Count = ${count}`, () => {
					testTeam({ format, rounds: 50 }, team => {
						for (const set of team) {
							let species = set.species;
							// Formes make this test code really complicated, so we skip them
							// (This is because info about formes isn't passed through)
							if (dex.species.get(species).otherFormes?.length || dex.species.get(species).forme) continue;
							if (set.gigantamax && !set.species.endsWith('max')) species += '-Gmax';

							// This is an array because the new set format can have multiple sets, and hence multiple possible
							// set lengths.
							const expectedMoves = [];
							for (const s of setsJSON[dex.species.get(species).id].sets) {
								let seenHP = false;
								let totalMoves = 0;
								for (const move of s.movepool) {
									if (move.startsWith('hiddenpower')) {
										if (seenHP) continue;
										seenHP = true;
									}
									totalMoves++;
								}
								expectedMoves.push(Math.min(totalMoves, count));
							}

							assert(expectedMoves.includes(set.moves.length), `${species} should have ${expectedMoves.toString()} moves (moves=${set.moves})`);
						}
					});
				});
			} else {
				const dataJSON = require(`../../dist/data/random-battles/gen${gen}/data.json`);

				it(`${format.name} should support Max Move Count = ${count}`, () => {
					testTeam({ format, rounds: 50 }, team => {
						for (const set of team) {
							let species = set.species;
							// Formes make this test code really complicated, so we skip them
							// (This is because info about formes isn't passed through)
							if (dex.species.get(species).otherFormes?.length || dex.species.get(species).forme) continue;
							if (set.gigantamax && !set.species.endsWith('max')) species += '-Gmax';

							// If the Pokémon has less than the max in its movepool, we should
							// just see all those moves.
							let totalMoves = 0;
							let seenHP = false;
							for (const move of dataJSON[dex.species.get(species).id].moves) {
								if (move.startsWith('hiddenpower')) {
									if (seenHP) continue;
									seenHP = true;
								}
								totalMoves++;
							}
							const expected = Math.min(totalMoves, count);
							assert.equal(set.moves.length, expected, `${species} should have ${expected} moves (moves=${set.moves})`);
						}
					});
				});
			}
		}
	}

	for (const format of Dex.formats.all()) {
		if (!format.team) continue;
		it(`${format.name} should support Adjust Level`, () => {
			const ruleTable = Dex.formats.getRuleTable(format);
			if (ruleTable.has('adjustleveldown') || ruleTable.has('adjustlevel')) return; // already adjusts level

			for (const level of [1, 99999]) {
				testTeam({ format: `${format.id}@@@Adjust Level = ${level}`, rounds: 50 }, team => {
					for (const set of team) {
						assert.equal(set.level, level);
					}
				});
			}
		});
	}
});

describe("New set format (slow)", () => {
	// formatInfo lists filenames and roles for each format
	const formatInfo = {
		"gen2randombattle": {
			filename: "gen2/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Bulky Attacker", "Bulky Setup", "Bulky Support", "Generalist", "Thief user"],
		},
		"gen3randombattle": {
			filename: "gen3/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Generalist", "Berry Sweeper"],
		},
		"gen4randombattle": {
			filename: "gen4/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "Spinner"],
		},
		"gen5randombattle": {
			filename: "gen5/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "Spinner"],
		},
		"gen6randombattle": {
			filename: "gen6/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "AV Pivot"],
		},
		"gen7randombattle": {
			filename: "gen7/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Z-Move user", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "AV Pivot"],
		},
		"gen8randombattle": {
			filename: "gen8/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Dynamax User", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "AV Pivot"],
		},
		"gen9randombattle": {
			filename: "gen9/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Fast Bulky Setup", "Bulky Support", "Fast Support", "AV Pivot"],
		},
		"gen9randomdoublesbattle": {
			filename: "gen9/doubles-sets",
			roles: ["Doubles Fast Attacker", "Doubles Setup Sweeper", "Doubles Wallbreaker", "Tera Blast user", "Doubles Bulky Attacker", "Doubles Bulky Setup", "Offensive Protect", "Bulky Protect", "Doubles Support", "Choice Item user"],
		},
		"gen9babyrandombattle": {
			filename: "gen9baby/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Bulky Support", "Fast Support"],
		},
		"gen9randombattle@@@+cap": {
			filename: "gen9cap/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Fast Bulky Setup", "Bulky Support", "Fast Support", "AV Pivot"],
		},
		"gen9freeforallrandombattle": {
			filename: "gen9ffa/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Bulky Support", "Fast Support", "AV Pivot", "Choice Item user", "Imprisoner"],
		},
		"gen9championsrandombattle": {
			filename: "champions/sets",
			roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Bulky Support", "Fast Support"],
		},
	};
	for (const format of Object.keys(formatInfo)) {
		const filename = formatInfo[format].filename;
		const setsJSON = require(`../../dist/data/random-battles/${filename}.json`);
		const dex = common.mod(common.getFormat({ formatid: format }).mod).dex; // verifies format exists
		const mod = (dex.currentMod === 'base') ? 'gen9' : dex.currentMod;
		const genNum = dex.gen;
		const rounds = 100;
		it(`${filename}.json should have valid set data`, () => {
			const validRoles = formatInfo[format].roles;
			const problems = [];
			for (const [id, sets] of Object.entries(setsJSON)) {
				const species = dex.species.get(id);
				if (!species.exists) problems.push(`misspelled species ID: ${id}`);
				assert(Array.isArray(sets.sets));
				for (const set of sets.sets) {
					if (!validRoles.includes(set.role)) problems.push(`set for ${species.name} has invalid role: ${set.role}`);
					for (const move of set.movepool) {
						const dexMove = dex.moves.get(move);
						if (!dexMove.exists) problems.push(`${species.name} has invalid move: ${move}`);
						// Old gens have moves in id form, currently.
						if (genNum >= 8) {
							if (move !== dexMove.name) problems.push(`${species.name} has misformatted move: ${move}`);
						} else {
							if (!(move === dexMove.id || move.startsWith('hiddenpower'))) {
								problems.push(`${species.name} has misformatted move: ${move}`);
							}
						}
						if (!validateLearnset(dexMove, { species }, 'ou', mod)) {
							problems.push(`${species.name} can't learn ${move}`);
						}
					}
					for (let i = 0; i < set.movepool.length - 1; i++) {
						if (set.movepool[i + 1] < set.movepool[i]) {
							problems.push(`${species.name} movepool should be sorted alphabetically`);
						}
					}
					if (genNum >= 3) {
						if (!set.abilities) problems.push(`${set.abilities} has no abilities`);
						for (const ability of set.abilities) {
							const dexAbility = dex.abilities.get(ability);
							if (!dexAbility.exists) problems.push(`${species.name} has invalid ability: ${ability}`);
							// Mega/Primal Pokemon have abilities from their base formes
							const allowedAbilities = new Set(Object.values((species.battleOnly && !species.requiredAbility) ? dex.species.get(species.battleOnly).abilities : species.abilities));
							if (species.unreleasedHidden) allowedAbilities.delete(species.abilities.H);
							if (!allowedAbilities.has(ability)) problems.push(`${species.name} can't have ${ability}`);
						}
						for (let i = 0; i < set.abilities.length - 1; i++) {
							if (set.abilities[i + 1] < set.abilities[i]) {
								problems.push(`${species.name} abilities should be sorted alphabetically`);
							}
						}
					}
					if (mod === 'gen9') {
						if (!set.teraTypes) problems.push(`${species.name} has no Tera Types`);
						for (const type of set.teraTypes) {
							const dexType = dex.types.get(type);
							if (!dexType.exists) problems.push(`${species.name} has invalid Tera Type: ${type}`);
							if (type !== dexType.name) problems.push(`${species.name} has misformatted Tera Type: ${type}`);
						}
						for (let i = 0; i < set.teraTypes.length - 1; i++) {
							if (set.teraTypes[i + 1] < set.teraTypes[i]) {
								problems.push(`${species.name} teraTypes should be sorted alphabetically`);
							}
						}
					}
					if (set.preferredTypes) {
						for (const type of set.preferredTypes) {
							const dexType = dex.types.get(type);
							if (!dexType.exists) problems.push(`${species.name} has invalid Preferred Type: ${type}`);
							if (type !== dexType.name) problems.push(`${species.name} has misformatted Preferred Type: ${type}`);
						}
						for (let i = 0; i < set.preferredTypes.length - 1; i++) {
							if (set.preferredTypes[i + 1] < set.preferredTypes[i]) {
								problems.push(`${species.name} preferredTypes should be sorted alphabetically`);
							}
						}
					}
				}
			}

			assert.false(!!problems.length, `In ${format},\n\t${problems.join('\n\t')}`);
		});
		it('all Pokemon should have 4 moves, except for Ditto and Unown', () => {
			testTeam({ format, rounds }, team => {
				for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.name === 'Unown' || pokemon.moves.length === 4, `In ${format}, ${pokemon.name} can generate with ${pokemon.moves.length} moves`);
			});
		});
		it('all moves on all sets should exist and be obtainable', () => {
			const generator = Teams.getGenerator(format);
			for (const pokemon of Object.keys(setsJSON)) {
				const species = dex.species.get(pokemon);
				assert(species.exists, `In ${format}, Pokemon ${species} does not exist`);
				const sets = setsJSON[pokemon]["sets"];
				const types = new Set(species.types);
				for (const set of sets) {
					assert(set.movepool.every(m => dex.moves.get(m).exists), `In ${format}, for Pokemon ${species}, one of ${set.movepool} does not exist.`);
					const role = set.role;
					const moves = new Set(set.movepool.map(m => (m.startsWith('hiddenpower') ? m : dex.moves.get(m).id)));
					const abilities = set.abilities || [];
					const specialTypes = mod === 'gen9' ? set.teraTypes : set.preferredTypes;
					// Go through all possible teamDetails combinations, if necessary
					for (let j = 0; j < rounds; j++) {
						// In Gens 2-3, if a set has multiple preferred types, we enforce moves of all the types.
						const specialType = specialTypes ? (genNum > 3 ? specialTypes[j % specialTypes.length] : specialTypes.join()) : '';
						// Generate a moveset for each combination of relevant teamDetails. Spikes is relevant for Gen 2.
						for (let i = 0; i < 16; i++) {
							const rapidSpin = i % 2;
							const stealthRock = Math.floor(i / 2) % 2;
							const stickyWeb = Math.floor(i / 4) % 2;
							const spikes = Math.floor(i / 8) % 2;
							const toxicSpikes = Math.floor(i / 4) % 2;
							const screens = Math.floor(i / 2) % 2;
							const teamDetails = { rapidSpin, stealthRock, stickyWeb, spikes, toxicSpikes, screens };
							// randomMoveset() deletes moves from the movepool, so recreate it every time
							const movePool = set.movepool.map(m => (m.startsWith('hiddenpower') ? m : dex.moves.get(m).id));
							let moveSet;
							if (mod === 'gen9') {
								moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, movePool, specialType, role, format.includes('doubles'));
							} else {
								moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, movePool, specialType, role);
							}
							for (const move of moveSet) moves.delete(move);
							if (!moves.size) break;
						}
						if (!moves.size) break;
					}
					assert.false(moves.size, `In ${format}, the following moves on ${species.name} are unused: ${[...moves].join(', ')}`);
				}
			}
		});
	}
});

describe('randomly generated teams should be valid (slow)', () => {
	for (const format of Dex.formats.all()) {
		if (!format.team) continue; // format doesn't use randomly generated teams
		if (format.mod === 'gen9ssb') continue; // Temporary

		it(`should generate valid ${format} teams`, function () {
			this.timeout(0);

			const targetTeamSize = Dex.formats.getRuleTable(format).maxTeamSize;
			testTeam({ format: format.id }, team => {
				assert.equal(team.length, targetTeamSize, `Team of incorrect size (should have ${targetTeamSize} Pokémon but actually has ${team.length} Pokémon): ${JSON.stringify(team)}`);

				for (const set of team) {
					assertSetValidity(format, set);
				}
			});
		});
	}
});

describe('Battle Factory and BSS Factory data should be valid (slow)', () => {
	for (const filename of ['gen9/factory-sets', 'gen8/factory-sets', 'gen8/bss-factory-sets', 'gen7/bss-factory-sets', 'gen7/factory-sets', 'gen6/factory-sets']) {
		it(`${filename}.json should contain valid sets`, function () {
			this.timeout(0);
			const setsJSON = require(`../../dist/data/random-battles/${filename}.json`);
			const mod = filename.split('/')[0] || 'gen' + Dex.gen;
			const genNum = isNaN(mod[3]) ? Dex.gen : parseInt(mod[3]);
			const dex = Dex.mod(mod);

			for (const type in setsJSON) {
				const typeTable = filename.includes('bss-factory-sets') ? setsJSON : setsJSON[type];
				let vType;
				if (filename.includes('bss-factory-sets')) {
					vType = `battle${genNum === 8 ? 'stadium' : 'spot'}singles`;
				} else if (type === 'Mono') {
					vType = 'monotype';
				} else if (type === 'Uber') {
					vType = 'ubers';
				} else {
					vType = type.toLowerCase();
				}
				for (const species in typeTable) {
					const speciesData = typeTable[species];
					for (const set of speciesData.sets) {
						const species = dex.species.get(set.species);
						assert(species.exists, `invalid species "${set.species}" of ${species}`);
						assert.equal(species.name, set.species, `miscapitalized species "${set.species}" of ${species}`);

						assert(species.id.startsWith(toID(species.baseSpecies)), `non-matching species "${set.species}" of ${species}`);

						for (const itemName of [].concat(set.item)) {
							if (!itemName) continue;
							const item = dex.items.get(itemName);
							assert(item.exists, `invalid item "${itemName}" of ${species}`);
							assert.equal(item.name, itemName, `miscapitalized item "${itemName}" of ${species}`);
						}

						for (const abilityName of [].concat(set.ability)) {
							const ability = dex.abilities.get(abilityName);
							assert(ability.exists, `invalid ability "${abilityName}" of ${species}`);
							assert.equal(ability.name, abilityName, `miscapitalized ability "${abilityName}" of ${species}`);
							const allowedAbilities = new Set(Object.values((species.battleOnly && !species.requiredAbility) ? dex.species.get(species.battleOnly).abilities : species.abilities));
							if (species.unreleasedHidden) allowedAbilities.delete(species.abilities.H);
							assert(allowedAbilities.has(abilityName), `${species.name} can't have ${abilityName}`);
						}

						for (const natureName of [].concat(set.nature)) {
							const nature = dex.natures.get(natureName);
							assert(nature.exists, `invalid nature "${natureName}" of ${species}`);
							assert.equal(nature.name, natureName, `miscapitalized nature "${natureName}" of ${species}`);
						}

						for (const moveSpec of set.moves) {
							for (const moveName of [].concat(moveSpec)) {
								const move = dex.moves.get(moveName);
								assert(move.exists, `invalid move "${moveName}" of ${species}`);
								assert.equal(move.name, moveName, `miscapitalized move "${moveName}" ≠ "${move.name}" of ${species}`);
								if (species.id === 'zacian' && [].concat(set.item).includes('Rusted Sword') && moveName === 'Behemoth Blade') continue;
								assert(validateLearnset(move, set, vType, mod), `illegal move "${moveName}" of ${species}`);
							}
						}

						// Check that no moves appear more than once in a set
						assert.equal(set.moves.flat(1).length, new Set(set.moves.flat(1)).size, `${species} has repeat moves`);

						assert(!!set.evs, `Set of ${species} has no EVs specified`);
						const keys = Object.keys(set.evs);
						let totalEVs = 0;
						for (const ev of keys) {
							assert(dex.stats.ids().includes(ev), `Invalid EV key (${ev}) on set of ${species}`);
							totalEVs += set.evs[ev];
							assert.equal(set.evs[ev] % 4, 0, `EVs of ${ev} not divisible by 4 on ${species}`);
						}
						const sortedKeys = Utils.sortBy([...keys], ev => dex.stats.ids().indexOf(ev));
						assert.deepEqual(keys, sortedKeys, `EVs out of order on set of ${species}, possibly because one of them is for the wrong stat`);
						assert(totalEVs <= 510, `more than 510 EVs on set of ${species}`);

						if (genNum === 9) {
							assert(set.teraType, `missing Tera Types on set of ${species}`);
							for (const type of set.teraType) {
								const dexType = dex.types.get(type);
								assert(dexType.exists, `${species} has invalid Tera Type: ${type}`);
								assert.equal(type, dexType.name, `${species.name} has misformatted Tera Type: ${type}`);
							}
						}
					}
					if (filename === 'gen9/factory-sets') {
						// Set weights should add up to 100
						let totalWeight = 0;
						for (const set of speciesData.sets) {
							totalWeight += set.weight;
						}
						assert.equal(totalWeight, 100, `Total set weight for ${species} is ${totalWeight < 100 ? 'less' : 'greater'} than 100%`);
					}
				}
			}
		});
	}
});

describe('[Gen 9] BSS Factory data should be valid (slow)', () => {
	it(`gen9/bss-factory-sets.json should contain valid sets`, function () {
		this.timeout(0);
		const setsJSON = require(`../../dist/data/random-battles/gen9/bss-factory-sets.json`);
		const mod = 'gen9';
		const genNum = 9;

		for (const speciesid in setsJSON) {
			const vType = 'bssregi';
			let totalWeight = 0;
			for (const set of setsJSON[speciesid].sets) {
				totalWeight += set.weight;
				const species = Dex.species.get(set.species);
				assert(species.exists, `invalid species "${set.species}" of ${speciesid}`);
				assert(!species.isNonstandard, `illegal species "${set.species}" of ${speciesid}`);
				assert.equal(species.name, set.species, `miscapitalized species "${set.species}" of ${speciesid}`);

				assert(species.id.startsWith(toID(species.baseSpecies)), `non-matching species "${set.species}" of ${speciesid}`);

				assert(!species.battleOnly, `invalid battle-only forme "${set.species}" of ${speciesid}`);

				for (const itemName of [].concat(set.item)) {
					if (!itemName && [].concat(...set.moves).includes("Acrobatics")) continue;
					const item = Dex.forGen(genNum).items.get(itemName);
					assert(item.exists, `invalid item "${itemName}" of ${speciesid}`);
					assert.equal(item.name, itemName, `miscapitalized item "${itemName}" of ${speciesid}`);
				}

				for (const abilityName of [].concat(set.ability)) {
					const ability = Dex.forGen(genNum).abilities.get(abilityName);
					assert(ability.exists, `invalid ability "${abilityName}" of ${speciesid}`);
					assert.equal(ability.name, abilityName, `miscapitalized ability "${abilityName}" of ${speciesid}`);
				}

				for (const natureName of [].concat(set.nature)) {
					const nature = Dex.forGen(genNum).natures.get(natureName);
					assert(nature.exists, `invalid nature "${natureName}" of ${speciesid}`);
					assert.equal(nature.name, natureName, `miscapitalized nature "${natureName}" of ${speciesid}`);
				}

				for (const moveSpec of set.moves) {
					for (const moveName of [].concat(moveSpec)) {
						const move = Dex.forGen(genNum).moves.get(moveName);
						assert(move.exists, `invalid move "${moveName}" of ${speciesid}`);
						assert.equal(move.name, moveName, `miscapitalized move "${moveName}" ≠ "${move.name}" of ${speciesid}`);
						assert(validateLearnset(move, set, vType, mod), `illegal move "${moveName}" of ${speciesid}`);
					}
				}

				assert(!!set.evs, `Set of ${speciesid} has no EVs specified`);
				const keys = Object.keys(set.evs);
				let totalEVs = 0;
				for (const ev of keys) {
					assert(Dex.stats.ids().includes(ev), `Invalid EV key (${ev}) on set of ${speciesid}`);
					totalEVs += set.evs[ev];
					assert.equal(set.evs[ev] % 4, 0, `EVs of ${ev} not divisible by 4 on ${speciesid}`);
				}
				const sortedKeys = Utils.sortBy([...keys], ev => Dex.stats.ids().indexOf(ev));
				assert.deepEqual(keys, sortedKeys, `EVs out of order on set of ${speciesid}, possibly because one of them is for the wrong stat`);
				assert(totalEVs <= 510, `more than 510 EVs on set of ${speciesid}`);
			}
			// Some species have 1/3 probability for each set
			if (totalWeight === 99) totalWeight += 1;
			assert.equal(totalWeight, 100, `Total set weight for ${speciesid} is ${totalWeight < 100 ? 'less' : 'greater'} than 100%`);
		}
	});
});

describe('[Gen 9] 1v1 Factory data should be valid (slow)', () => {
	it(`gen9/1v1-factory-sets.json should contain valid sets`, function () {
		this.timeout(0);
		const setsJSON = require(`../../dist/data/random-battles/gen9/1v1-factory-sets.json`);
		const mod = 'gen9';

		for (const speciesName in setsJSON) {
			const speciesData = setsJSON[speciesName];
			for (const set of speciesData.sets) {
				const species = Dex.species.get(set.species);
				assert(species.exists, `invalid species "${set.species}" of ${speciesName}`);
				assert.equal(species.name, set.species, `miscapitalized species "${set.species}" of ${speciesName}`);

				assert(species.id.startsWith(toID(species.baseSpecies)), `non-matching species "${set.species}" of ${speciesName}`);

				for (const itemName of [].concat(set.item)) {
					if (!itemName) continue;
					const item = Dex.items.get(itemName);
					assert(item.exists, `invalid item "${itemName}" of ${speciesName}`);
					assert.equal(item.name, itemName, `miscapitalized item "${itemName}" of ${speciesName}`);
				}

				for (const abilityName of [].concat(set.ability)) {
					const ability = Dex.abilities.get(abilityName);
					assert(ability.exists, `invalid ability "${abilityName}" of ${speciesName}`);
					assert.equal(ability.name, abilityName, `miscapitalized ability "${abilityName}" of ${speciesName}`);
					const allowedAbilities = new Set(Object.values((species.battleOnly && !species.requiredAbility) ? Dex.species.get(species.battleOnly).abilities : species.abilities));
					if (species.unreleasedHidden) allowedAbilities.delete(species.abilities.H);
					assert(allowedAbilities.has(abilityName), `${speciesName} can't have ${abilityName}`);
				}

				for (const natureName of [].concat(set.nature)) {
					const nature = Dex.natures.get(natureName);
					assert(nature.exists, `invalid nature "${natureName}" of ${speciesName}`);
					assert.equal(nature.name, natureName, `miscapitalized nature "${natureName}" of ${speciesName}`);
				}

				for (const moveSpec of set.moves) {
					for (const moveName of [].concat(moveSpec)) {
						const move = Dex.moves.get(moveName);
						assert(move.exists, `invalid move "${moveName}" of ${speciesName}`);
						assert.equal(move.name, moveName, `miscapitalized move "${moveName}" ≠ "${move.name}" of ${speciesName}`);
						assert(validateLearnset(move, set, '1v1', mod), `illegal move "${moveName}" of ${speciesName}`);
					}
				}

				// Check that no moves appear more than once in a set
				assert.equal(set.moves.flat(1).length, new Set(set.moves.flat(1)).size, `${speciesName} has repeat moves`);

				if (speciesName === 'Carbink') continue;
				assert(!!set.evs, `Set of ${speciesName} has no EVs specified`);
				const keys = Object.keys(set.evs);
				let totalEVs = 0;
				for (const ev of keys) {
					assert(Dex.stats.ids().includes(ev), `Invalid EV key (${ev}) on set of ${speciesName}`);
					totalEVs += set.evs[ev];
					assert.equal(set.evs[ev] % 4, 0, `EVs of ${ev} not divisible by 4 on ${speciesName}`);
				}
				const sortedKeys = Utils.sortBy([...keys], ev => Dex.stats.ids().indexOf(ev));
				assert.deepEqual(keys, sortedKeys, `EVs out of order on set of ${speciesName}, possibly because one of them is for the wrong stat`);
				assert(totalEVs <= 510, `more than 510 EVs on set of ${speciesName}`);
			}
			let totalWeight = 0;
			for (const set of speciesData.sets) {
				totalWeight += set.weight;
			}
			assert.equal(totalWeight, 100, `Total set weight for ${speciesName} is ${totalWeight < 100 ? 'less' : 'greater'} than 100%`);
		}
	});
});
