#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "algos/matrix.hpp"
#include "algos/rand.hpp"
#include "constants.hpp"
#include "algos/maze.hpp"
#include "algos/stats.hpp"

#define DUMP 0

using std::string;
using matrix::Matrix;

TEST_CASE("hunt-and-kill", "[mazes]") {
  Map map(21, 21);
  maze::Builder maze(map);

  maze.hunt_and_kill();
  REQUIRE(maze.repair() == true);

  if(DUMP) maze.dump("BASIC MAZE");

  maze.randomize_rooms(ROOM_SIZE);
  maze.hunt_and_kill();
  maze.place_doors();
  REQUIRE(maze.repair() == true);

  if(DUMP) maze.dump("ROOM MAZE");

  REQUIRE(map.$dead_ends.size() > 0);
  REQUIRE(map.$rooms.size() > 0);
}

TEST_CASE("hunt-and-kill box", "[mazes]") {
  for(int i = 25; i < 65; i += 2) {
    Map map(i, i);
    maze::Builder maze(map);

    maze.hunt_and_kill();
    maze.clear();
    maze.inner_box(6, 4);
    maze.randomize_rooms(ROOM_SIZE);

    maze.hunt_and_kill();
    maze.open_box(6);
    maze.place_doors();
    auto valid = maze.repair();

    if(i == 41 && DUMP) {
      maze.dump(valid ? "INNER BOX" : "FAILED BOX");
    }
  }
}

TEST_CASE("hunt-and-kill ring", "[mazes]") {
  Map map(21, 21);
  maze::Builder maze(map);

  maze.inner_donut(5.5, 3.5);
  maze.hunt_and_kill();
  REQUIRE(maze.repair() == true);

  if(DUMP) maze.dump("INNER RING");

  REQUIRE(maze.$rooms.size() == 0);
}

TEST_CASE("hunt-and-kill fissure", "[mazes]") {
  Map map(21, 21);
  maze::Builder maze(map);

  maze.divide({3,3}, {19,18});
  maze.hunt_and_kill();
  REQUIRE(maze.repair() == true);

  if(DUMP) maze.dump("FISSURE MAZE");

  REQUIRE(maze.$rooms.size() == 0);
}

TEST_CASE("hunt-and-kill no-dead-ends", "[mazes]") {
  Map map(21, 21);
  maze::Builder maze(map);

  maze.hunt_and_kill();
  maze.remove_dead_ends();
  REQUIRE(maze.repair() == true);

  if(DUMP) maze.dump("NO DEAD ENDS");
}

TEST_CASE("hunt-and-kill too much", "[mazes]") {
  for(int i = 25; i < 65; i += 2) {
    Map map(i, i);
    maze::Builder maze(map);

    maze.hunt_and_kill();
    maze.randomize_rooms(ROOM_SIZE);
    maze.clear();
    maze.inner_donut(9, 4);
    maze.divide({3,3}, {15,16});
    maze.hunt_and_kill();
    maze.place_doors();
    auto valid = maze.repair();

    if(i == 41 && DUMP && valid) {
      maze.dump("COMBINED");
    }
  }
}

TEST_CASE("hunt-and-kill validator", "[mazes]") {
  bool valid = true;
  Stats mofm;

  for(int i = 0; i < 10; i++) {
    Stats door_prob;

    do {
      Map map(33, 33);
      maze::Builder maze(map);

      maze.hunt_and_kill();
      maze.clear();
      maze.inner_box(6, 4);
      maze.randomize_rooms(ROOM_SIZE);
      maze.hunt_and_kill();
      maze.open_box(6);
      maze.place_doors();
      valid = maze.repair();

      if(i == 9 && DUMP) {
        maze.dump(valid ? "VALIDATED" : "FAILED!");
      }
      door_prob.sample(valid);
    } while(!valid);

    if(DUMP) door_prob.dump();
    mofm.sample(door_prob.mean());
  }

  if(DUMP) {
    fmt::println("FINAL m-of-m");
    mofm.dump();
  }

  REQUIRE(mofm.mean() > 0.20);
}


TEST_CASE("hunt-and-kill scripting", "[mazes]") {
  using namespace nlohmann::literals;

  // go up by 2 to keep odd
  for(int i = 0; i < 20; i+=2) {
    auto script = R"(
      [
      {"action": "hunt_and_kill"},
      {"action": "clear"},
      {"action": "inner_box", "data": [6, 4]},
      {"action": "randomize_rooms", "data": [4]},
      {"action": "divide", "data": [3, 3, 10, 10]},
      {"action": "inner_donut", "data": [5.5,4.5]},
      {"action": "hunt_and_kill"},
      {"action": "open_box", "data": [6]},
      {"action": "remove_dead_ends"},
      {"action": "place_doors"}
      ]
    )"_json;

    Map map(23+i, 23+i);
    auto [maze, valid] = maze::script(map, script);

    if(valid) {
      REQUIRE(maze.validate() == true);
      REQUIRE(map.INVARIANT() == true);
    }

    if(DUMP) maze.dump(valid ? "SCRIPTED" : "SCRIPTED FAIL!");
  }
}
