// Copyright (c) 2017 PSForever
package objects

import net.psforever.objects._
import net.psforever.objects.vital.damage.{DamageProfile, _}
import net.psforever.objects.vital.projectile._
import DamageCalculations._
import net.psforever.objects.vital.resistance.ResistanceCalculations
import ResistanceCalculations._
import net.psforever.objects.vital.resolution.ResolutionCalculations
import ResolutionCalculations._
import net.psforever.objects.ballistics._
import net.psforever.objects.definition.{ProjectileDefinition, VehicleDefinition}
import net.psforever.objects.vital.Vitality
import net.psforever.packet.game.objectcreate.ObjectClass
import net.psforever.types._
import org.specs2.mutable.Specification
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.interaction.DamageInteraction

class DamageCalculationsTests extends Specification {
  "DamageCalculations" should {
    val wep        = GlobalDefinitions.galaxy_gunship_cannon
    val wep_fmode  = Tool(wep).FireMode
    val wep_prof   = wep_fmode.Add
    val proj       = DamageModelTests.projectile
    val proj_prof  = proj.asInstanceOf[DamageProfile]
    val player     = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
    val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
    val target     = Vehicle(GlobalDefinitions.fury)
    target.Position = Vector3(10, 0, 0)
    val resprojectile = DamageInteraction(
      SourceEntry(target),
      ProjectileReason(
        DamageResolution.Hit,
        projectile,
        target.DamageModel
      ),
      Vector3(15, 0, 0)
    )

    "extract no damage numbers" in {
      AgainstNothing(proj_prof) mustEqual 0
    }

    "extract damage against exosuit target" in {
      AgainstExoSuit(proj_prof) == proj_prof.Damage0 mustEqual true
    }

    "extract damage against MAX target" in {
      AgainstMaxSuit(proj_prof) == proj_prof.Damage3 mustEqual true
    }

    "extract damage against vehicle target" in {
      AgainstVehicle(proj_prof) == proj_prof.Damage1 mustEqual true
    }

    "extract damage against aircraft target" in {
      AgainstAircraft(proj_prof) == proj_prof.Damage2 mustEqual true
    }

    "extract damage against battleframe robotics" in {
      AgainstBfr(proj_prof) == proj_prof.Damage4 mustEqual true
    }

    "no degrade damage modifier" in {
      SameHit.calculate(100, resprojectile) mustEqual 100
    }

    "degrade over distance damage modifier (no degrade)" in {
      DistanceDegrade.calculate(100, resprojectile) == 100 mustEqual true
    }

    "cut off damage at max distance (no cutoff)" in {
      MaxDistanceCutoff.calculate(100, resprojectile) == 100 mustEqual true
    }

    "cut off damage at max distance (cutoff)" in {
      val cutoffprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target.DamageModel
        ),
        Vector3(1500, 0, 0)
      )
      val damage = MaxDistanceCutoff.calculate(100, cutoffprojectile)
      damage == 0 mustEqual true
    }

    "cut off damage at custom distance (no cutoff)" in {
      CustomDistanceCutoff(10).calculate(100, resprojectile) == 0 mustEqual true
    }

    "cut off damage at custom distance (cutoff)" in {
      val coffprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          projectile,
          target.DamageModel
        ),
        Vector3(10, 0, 0)
      )
      CustomDistanceCutoff(2).calculate(100, coffprojectile) == 0 mustEqual true
    }

    "degrade over distance damage modifier (some degrade)" in {
      val resprojectile2 = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          projectile,
          target.DamageModel
        ),
        Vector3(100, 0, 0)
      )
      val damage = DistanceDegrade.calculate(100, resprojectile2)
      damage < 100 && damage > 0 mustEqual true
    }

    "degrade over distance damage modifier (zero'd)" in {
      val resprojectile2 = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          projectile,
          target.DamageModel
        ),
        Vector3(1000, 0, 0)
      )
      DistanceDegrade.calculate(100, resprojectile2) == 0 mustEqual true
    }

    "degrade at radial distance damage modifier (no degrade)" in {
      val resprojectile2 = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          projectile,
          target.DamageModel
        ),
        Vector3(10, 0, 0)
      )
      RadialDegrade.calculate(100, resprojectile2) == 100 mustEqual true
    }

    "degrade at radial distance damage modifier (some degrade)" in {
      val resprojectile2 = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          projectile,
          target.DamageModel
        ),
        Vector3(12, 0, 0)
      )
      val damage = RadialDegrade.calculate(100, resprojectile2)
      damage < 100 && damage > 0 mustEqual true
    }

    "degrade at radial distance damage modifier (zero'd)" in {
      val resprojectile2 = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          projectile,
          target.DamageModel
        ),
        Vector3(100, 0, 0)
      )
      RadialDegrade.calculate(100, resprojectile2) == 0 mustEqual true
    }

    "lash degrade (no lash; too close)" in {
      val resprojectile2 = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Lash,
          projectile,
          target.DamageModel
        ),
        Vector3(5, 0, 0) //compared to Vector3(2, 2, 0)
      )
      Lash.calculate(100, resprojectile2) == 0 mustEqual true
    }

    "lash degrade (lash)" in {
      val resprojectile2 = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Lash,
          projectile,
          target.DamageModel
        ),
        Vector3(20, 0, 0)
      )
      val damage = Lash.calculate(100, resprojectile2)
      damage < 100 && damage > 0 mustEqual true
    }

    "lash degrade (no lash; too far)" in {
      val resprojectile2 = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Lash,
          projectile,
          target.DamageModel
        ),
        Vector3(1000, 0, 0)
      )
      Lash.calculate(100, resprojectile2) == 0 mustEqual true
    }

    "fireball aggravated damage (aggravated splash burn" in {
//      val burnWeapon = Tool(GlobalDefinitions.flamethrower)
//      val burnProjectile = Projectile(
//        burnWeapon.Projectile,
//        burnWeapon.Definition,
//        burnWeapon.FireMode,
//        player,
//        Vector3(2, 2, 0),
//        Vector3.Zero
//      )
      val burnRes = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.AggravatedSplashBurn,
          projectile,
          target.DamageModel
        ),
        Vector3(15, 0, 0)
      )
      val resistance = target.DamageModel.ResistUsing(burnRes)(burnRes)
      FireballAggravatedBurn.calculate(100, burnRes) == (1 + resistance) mustEqual true
    }

    "fireball aggravated damage (noral splash, no modification)" in {
//      val burnWeapon = Tool(GlobalDefinitions.flamethrower)
//      val burnProjectile = Projectile(
//        burnWeapon.Projectile,
//        burnWeapon.Definition,
//        burnWeapon.FireMode,
//        player,
//        Vector3(2, 2, 0),
//        Vector3.Zero
//      )
      val burnRes = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          projectile,
          target.DamageModel
        ),
        Vector3(15, 0, 0)
      )
      FireballAggravatedBurn.calculate(100, burnRes) == 100 mustEqual true
    }

    val charge_weapon = Tool(GlobalDefinitions.spiker)
    val charge_projectile = Projectile(
      charge_weapon.Projectile,
      charge_weapon.Definition,
      charge_weapon.FireMode,
      player,
      Vector3(2, 2, 0),
      Vector3.Zero
    )
    val minDamageBase = charge_weapon.Projectile.Charging.get.min.Damage0
    val chargeBaseDamage = charge_weapon.Projectile.Damage0

    "charge (none)" in {
      val cprojectile = charge_projectile.quality(ProjectileQuality.Modified(0))
      val rescprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          cprojectile,
          target.DamageModel
        ),
        Vector3(15, 0, 0)
      )
      val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
      val calcDam = minDamageBase + math.floor(
        chargeBaseDamage * rescprojectile.cause.asInstanceOf[ProjectileReason].projectile.quality.mod
      )
      damage mustEqual calcDam
    }

    "charge (half)" in {
      val cprojectile = charge_projectile.quality(ProjectileQuality.Modified(0.5f))
      val rescprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          cprojectile,
          target.DamageModel
        ),
        Vector3(15, 0, 0)
      )
      /*val damage = */SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
      ok
    }

    "charge (full)" in {
      val cprojectile = charge_projectile.quality(ProjectileQuality.Modified(1))
      val rescprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          cprojectile,
          target.DamageModel
        ),
        Vector3(15, 0, 0)
      )
      /*val damage = */SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
      ok
    }

    val flak_weapon = Tool(GlobalDefinitions.trhev_burster)
    val flak_projectile = Projectile(
      flak_weapon.Projectile,
      flak_weapon.Definition,
      flak_weapon.FireMode,
      player,
      Vector3(2, 2, 0),
      Vector3.Zero
    )

    "flak hit (resolution is splash, no degrade)" in {
      val resfprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          flak_projectile,
          target.DamageModel
        ),
        Vector3(10, 0, 0)
      )
      val damage = FlakHit.calculate(100, resfprojectile)
      damage == 100 mustEqual true
    }

    "flak hit (resolution is hit, no degrade)" in {
      val resfprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          flak_projectile,
          target.DamageModel
        ),
        Vector3(10, 0, 0)
      )
      val damage = FlakHit.calculate(100, resfprojectile)
      damage == 100 mustEqual true
    }

    "flak burst (resolution is hit)" in {
      val resfprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          flak_projectile,
          target.DamageModel
        ),
        Vector3(15, 0, 0)
      )
      val damage = FlakBurst.calculate(100, resfprojectile)
      damage == 100 mustEqual true
    }

    "flak burst (resolution is splash, no degrade)" in {
      val resfprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          flak_projectile,
          target.DamageModel
        ),
        Vector3(10, 0, 0)
      )
      val damage = FlakBurst.calculate(100, resfprojectile)
      damage == 100 mustEqual true
    }

    "flak burst (resolution is splash, some degrade)" in {
      val resfprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Splash,
          flak_projectile,
          target.DamageModel
        ),
        Vector3(5, 0, 0)
      )
      val damage = FlakBurst.calculate(100, resfprojectile)
      damage < 100 mustEqual true
    }

    "galaxy gunship reduction (target is galaxy_gunship, no shields)" in {
      val vehicle = Vehicle(GlobalDefinitions.galaxy_gunship)
      val resfprojectile = DamageInteraction(
        SourceEntry(vehicle),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          vehicle.DamageModel
        ),
        Vector3(5, 0, 0)
      )
      val damage = GalaxyGunshipReduction(0.63f).calculate(100, resfprojectile)
      damage == 63 mustEqual true
    }

    "galaxy gunship reduction (target is galaxy_gunship)" in {
      val vehicle = Vehicle(GlobalDefinitions.galaxy_gunship)
      vehicle.Shields = 1
      val resfprojectile = DamageInteraction(
        SourceEntry(vehicle),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          vehicle.DamageModel
        ),
        Vector3(5, 0, 0)
      )
      val damage = GalaxyGunshipReduction(0.63f).calculate(100, resfprojectile)
      damage == 100 mustEqual true
    }

    "galaxy gunship reduction (target is vehicle, but not a galaxy_gunship)" in {
      val resfprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target.DamageModel
        ),
        Vector3(5, 0, 0)
      )
      val damage = GalaxyGunshipReduction(0.63f).calculate(100, resfprojectile)
      damage == 100 mustEqual true
    }

    "galaxy gunship reduction (target is not a vehicle)" in {
      val tplayer =
        Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
      val resfprojectile = DamageInteraction(
        SourceEntry(tplayer),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          tplayer.DamageModel
        ),
        Vector3(5, 0, 0)
      )
      val damage = GalaxyGunshipReduction(0.63f).calculate(100, resfprojectile)
      damage == 100 mustEqual true
    }

    "extract a complete damage profile" in {
      val result1 = RadialDegrade.calculate(
        AgainstVehicle(proj_prof) + AgainstVehicle(wep_prof),
        resprojectile
      )
      val result2 = DamageCalculations.WithModifiers(AgainstVehicle, resprojectile)
      result1 mustEqual result2
    }
  }
}

class ResistanceCalculationsTests extends Specification {
  val wep        = GlobalDefinitions.galaxy_gunship_cannon
  val wep_fmode  = Tool(wep).FireMode
  val proj       = DamageModelTests.projectile
  val player     = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
  val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)

  "ResistanceCalculations" should {
    "ignore all targets" in {
      val target = Vehicle(GlobalDefinitions.fury)
      val resprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target.DamageModel
        ),
        Vector3.Zero
      )
      InvalidTarget(resprojectile).isFailure mustEqual true
    }

    "discern standard infantry targets" in {
      val target = player
      val resprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target.DamageModel
        ),
        Vector3.Zero
      )
      ValidInfantryTarget(resprojectile).isSuccess mustEqual true
      ValidMaxTarget(resprojectile).isSuccess mustEqual false
      ValidVehicleTarget(resprojectile).isSuccess mustEqual false
      ValidAircraftTarget(resprojectile).isSuccess mustEqual false
    }

    "discern mechanized infantry targets" in {
      val target = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
      target.ExoSuit = ExoSuitType.MAX
      val resprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target.DamageModel
        ),
        Vector3.Zero
      )
      ValidInfantryTarget(resprojectile).isSuccess mustEqual false
      ValidMaxTarget(resprojectile).isSuccess mustEqual true
      ValidVehicleTarget(resprojectile).isSuccess mustEqual false
      ValidAircraftTarget(resprojectile).isSuccess mustEqual false
    }

    "discern ground vehicle targets" in {
      val target = Vehicle(GlobalDefinitions.fury)
      val resprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target.DamageModel
        ),
        Vector3.Zero
      )
      ValidInfantryTarget(resprojectile).isSuccess mustEqual false
      ValidMaxTarget(resprojectile).isSuccess mustEqual false
      ValidVehicleTarget(resprojectile).isSuccess mustEqual true
      ValidAircraftTarget(resprojectile).isSuccess mustEqual false
    }

    "discern flying vehicle targets" in {
      val target = Vehicle(GlobalDefinitions.mosquito)
      val resprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target.DamageModel
        ),
        Vector3.Zero
      )
      ValidInfantryTarget(resprojectile).isSuccess mustEqual false
      ValidMaxTarget(resprojectile).isSuccess mustEqual false
      ValidVehicleTarget(resprojectile).isSuccess mustEqual false
      ValidAircraftTarget(resprojectile).isSuccess mustEqual true
    }

    "extract no resistance values" in {
      NoResistExtractor(SourceEntry(player)) mustEqual 0
    }

    "extract resistance values from exo-suit" in {
      val pSource = PlayerSource(player)
      ExoSuitDirectExtractor(pSource) mustEqual 4
      ExoSuitSplashExtractor(pSource) mustEqual 15
      ExoSuitAggravatedExtractor(pSource) mustEqual 8
      ExoSuitRadiationExtractor(pSource) mustEqual 0
    }

    "extract resistance values from vehicle" in {
      val vSource = VehicleSource(Vehicle(GlobalDefinitions.fury))
      VehicleDirectExtractor(vSource) mustEqual 0
      VehicleSplashExtractor(vSource) mustEqual 0
      VehicleAggravatedExtractor(vSource) mustEqual 0
      VehicleRadiationExtractor(vSource) mustEqual 0
    }
  }
}

class ResolutionCalculationsTests extends Specification {
  val wep       = GlobalDefinitions.galaxy_gunship_cannon
  val wep_fmode = Tool(wep).FireMode
  val proj      = DamageModelTests.projectile
  val player    = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
  player.Spawn()
  val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)

  "ResolutionCalculations" should {
    "calculate no damage" in {
      val target = player
      val resprojectile = DamageInteraction(
        SourceEntry(target),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target.DamageModel
        ),
        Vector3.Zero
      )
      ResolutionCalculations.NoDamage(resprojectile)(50, 50) mustEqual 0
    }

    "calculate no infantry damage for vehicles" in {
      val target1 = Vehicle(GlobalDefinitions.fury) //!
      val resprojectile1 = DamageInteraction(
        SourceEntry(target1),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target1.DamageModel
        ),
        Vector3.Zero
      )
      InfantryDamage(resprojectile1)(50, 10) mustEqual (0, 0)

      val target2 = player
      val resprojectile2 = DamageInteraction(
        SourceEntry(target2),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target2.DamageModel
        ),
        Vector3.Zero
      )
      InfantryDamage(resprojectile2)(50, 10) mustEqual (40, 10)
    }

    "calculate health and armor damage for infantry target" in {
      InfantryDamageAfterResist(100, 100)(50, 10) mustEqual (40, 10)
    }

    "calculate health and armor damage, with bleed through damage, for infantry target" in {
      //health = 100, armor = 5 -> resist 10 but only have 5, so rollover extra -> damages (40+5, 5)
      InfantryDamageAfterResist(100, 5)(50, 10) mustEqual (45, 5)
    }

    "calculate health damage for infantry target" in {
      //health = 100, armor = 0
      InfantryDamageAfterResist(100, 0)(50, 10) mustEqual (50, 0)
    }

    "calculate armor damage for infantry target" in {
      //resistance > damage
      InfantryDamageAfterResist(100, 100)(50, 60) mustEqual (0, 50)
    }

    val player2 = Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
    player2.ExoSuit = ExoSuitType.MAX
    player2.Spawn()
    "calculate no max damage for vehicles" in {
      val target1 = Vehicle(GlobalDefinitions.fury) //!
      val resprojectile1 = DamageInteraction(
        SourceEntry(target1),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target1.DamageModel
        ),
        Vector3.Zero
      )
      MaxDamage(resprojectile1)(50, 10) mustEqual (0, 0)

      val target2 = player2
      val resprojectile2 = DamageInteraction(
        SourceEntry(target2),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target2.DamageModel
        ),
        Vector3.Zero
      )
      MaxDamage(resprojectile2)(50, 10) mustEqual (0, 40)
    }

    "calculate health and armor damage for max target" in {
      MaxDamageAfterResist(100, 5)(50, 10) mustEqual (35, 5)
    }

    "calculate health damage for max target" in {
      //health = 100, armor = 0
      MaxDamageAfterResist(100, 0)(50, 10) mustEqual (40, 0)
    }

    "calculate armor damage for max target" in {
      //resistance > damage
      MaxDamageAfterResist(100, 100)(50, 10) mustEqual (0, 40)
    }

    "do not care if target is infantry for vehicle calculations" in {
      val target1 = player
      val resprojectile1 = DamageInteraction(
        SourceEntry(target1),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target1.DamageModel
        ),
        Vector3.Zero
      )
      VehicleDamageAfterResist(resprojectile1)(50, 10) mustEqual 40

      val target2 = Vehicle(GlobalDefinitions.fury) //!
      val resprojectile2 = DamageInteraction(
        SourceEntry(target2),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          target2.DamageModel
        ),
        Vector3.Zero
      )
      VehicleDamageAfterResist(resprojectile2)(50, 10) mustEqual 40
    }

    "calculate resisted damage for vehicle target" in {
      VehicleDamageAfterResist(50, 10) mustEqual 40
    }

    "calculate un-resisted damage for vehicle target" in {
      VehicleDamageAfterResist(50, 0) mustEqual 50
    }
  }
}

class DamageModelTests extends Specification {
  val wep       = GlobalDefinitions.galaxy_gunship_cannon
  val wep_tool  = Tool(wep)
  val wep_fmode = wep_tool.FireMode
  val proj      = DamageModelTests.projectile
  val player    = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
  player.Spawn()
  val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)

  "DamageModel" should {
    "be a part of vitality" in {
      player.isInstanceOf[Vitality] mustEqual true
      try {
        player.getClass.getDeclaredMethod("DamageModel").hashCode()
      } catch {
        case _: Exception =>
          ko //the method doesn't exist
      }

      wep_tool.isInstanceOf[Vitality] mustEqual false
      try {
        wep_tool.getClass.getDeclaredMethod("DamageModel").hashCode()
        ko
      } catch {
        case _: Exception =>
          ok //the method doesn't exist
      }
      ok
    }

    "resolve infantry targets" in {
      val tplayer =
        Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
      tplayer.Spawn()
      tplayer.Health mustEqual 100
      tplayer.Armor mustEqual 50

      val resprojectile = DamageInteraction(
        SourceEntry(tplayer),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          tplayer.DamageModel
        ),
        Vector3.Zero
      )
      val func = resprojectile.calculate()
      func(tplayer)
      tplayer.Health mustEqual 65
      tplayer.Armor mustEqual 35
    }

    "resolve infantry targets in a specific way" in {
      val tplayer =
        Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
      tplayer.Spawn()
      tplayer.Health mustEqual 100
      tplayer.Armor mustEqual 50

      val resprojectile = DamageInteraction(
        SourceEntry(tplayer),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          tplayer.DamageModel
        ),
        Vector3.Zero
      )
      val func = resprojectile.calculate(DamageType.Splash)
      func(tplayer)
      tplayer.Health mustEqual 65
      tplayer.Armor mustEqual 35
    }

    "resolve infantry targets, with damage overflow" in {
      val tplayer =
        Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
      tplayer.Spawn()
      tplayer.Health mustEqual 100
      tplayer.Armor mustEqual 50

      val resprojectile = DamageInteraction(
        SourceEntry(tplayer),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          tplayer.DamageModel
        ),
        Vector3.Zero
      )
      val func = resprojectile.calculate()
      tplayer.Armor = 0

      func(tplayer)
      tplayer.Health mustEqual 50
      tplayer.Armor mustEqual 0
    }

    "resolve vehicle targets" in {
      val vehicle = Vehicle(DamageModelTests.vehicle)
      vehicle.Health mustEqual 650

      val resprojectile = DamageInteraction(
        SourceEntry(vehicle),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          vehicle.DamageModel
        ),
        Vector3.Zero
      )
      val func = resprojectile.calculate()

      func(vehicle)
      vehicle.Health mustEqual 518
    }

    "resolve vehicle targets (with shields)" in {
      val vehicle = Vehicle(DamageModelTests.vehicle)
      vehicle.Shields = 10
      vehicle.Health mustEqual 650
      vehicle.Shields mustEqual 10

      val resprojectile = DamageInteraction(
        SourceEntry(vehicle),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          vehicle.DamageModel
        ),
        Vector3.Zero
      )
      val func = resprojectile.calculate()

      func(vehicle)
      vehicle.Health mustEqual 528
      vehicle.Shields mustEqual 0
    }

    "resolve vehicle targets (losing shields)" in {
      val vehicle = Vehicle(DamageModelTests.vehicle)
      vehicle.Shields = 10
      vehicle.Health mustEqual 650
      vehicle.Shields mustEqual 10

      val resprojectile = DamageInteraction(
        SourceEntry(vehicle),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          vehicle.DamageModel
        ),
        Vector3.Zero
      )
      val func = resprojectile.calculate()

      func(vehicle)
      vehicle.Health mustEqual 528
      vehicle.Shields mustEqual 0
      func(vehicle)
      vehicle.Health mustEqual 396
      vehicle.Shields mustEqual 0
    }

    "resolve vehicle targets in a specific way" in {
      val vehicle = Vehicle(DamageModelTests.vehicle)
      vehicle.Health mustEqual 650

      val resprojectile = DamageInteraction(
        SourceEntry(vehicle),
        ProjectileReason(
          DamageResolution.Hit,
          projectile,
          vehicle.DamageModel
        ),
        Vector3.Zero
      )
      val func = resprojectile.calculate(DamageType.Splash)

      func(vehicle)
      vehicle.Health mustEqual 518
    }
  }
}

object DamageModelTests {
  final val projectile = new ProjectileDefinition(Projectiles.Types.heavy_grenade_projectile.id) {
    Damage0 = 50
    Damage1 = 82
    Damage2 = 82
    Damage3 = 75
    Damage4 = 66
    DamageAtEdge = 0.1f
    DamageRadius = 5f
    DegradeMultiplier = 0.5f
    LashRadius = 5f
    ProjectileDamageType = DamageType.Splash
    InitialVelocity = 75
    Lifespan = 5f
    ProjectileDefinition.CalculateDerivedFields(pdef = this)
    Modifiers = RadialDegrade
  }

  final val vehicle = new VehicleDefinition(ObjectClass.fury) {
    MaxHealth = 650
    Damageable = true
    Repairable = true
    RepairIfDestroyed = false
    MaxShields = 130 + 1
  }
}
