#include <gtest/gtest.h>
#include <memory>
#include <vector>
#include <filesystem>
#include <string>

#include "app/rom.h"
#include "app/zelda3/overworld/overworld.h"
#include "app/zelda3/overworld/overworld_map.h"
#include "testing.h"

namespace yaze {
namespace zelda3 {

/**
 * @brief Comprehensive overworld integration test that validates YAZE C++ 
 *        implementation against ZScream C# logic and existing test infrastructure
 * 
 * This test suite:
 * 1. Validates overworld loading logic matches ZScream behavior
 * 2. Tests integration with ZSCustomOverworld versions (vanilla, v2, v3)
 * 3. Uses existing RomDependentTestSuite infrastructure when available
 * 4. Provides both mock data and real ROM testing capabilities
 */
class OverworldIntegrationTest : public ::testing::Test {
 protected:
  void SetUp() override {
#if defined(__linux__)
    GTEST_SKIP();
#endif
    
    // Check if we should use real ROM or mock data
    const char* rom_path_env = getenv("YAZE_TEST_ROM_PATH");
    const char* skip_rom_tests = getenv("YAZE_SKIP_ROM_TESTS");
    
    if (skip_rom_tests) {
      GTEST_SKIP() << "ROM tests disabled";
    }
    
    if (rom_path_env && std::filesystem::exists(rom_path_env)) {
      // Use real ROM for testing
      rom_ = std::make_unique<Rom>();
      auto status = rom_->LoadFromFile(rom_path_env);
      if (status.ok()) {
        use_real_rom_ = true;
        overworld_ = std::make_unique<Overworld>(rom_.get());
        return;
      }
    }
    
    // Fall back to mock data
    use_real_rom_ = false;
    rom_ = std::make_unique<Rom>();
    SetupMockRomData();
    rom_->LoadFromData(mock_rom_data_);
    overworld_ = std::make_unique<Overworld>(rom_.get());
  }

  void TearDown() override {
    overworld_.reset();
    rom_.reset();
  }

  void SetupMockRomData() {
    mock_rom_data_.resize(0x200000, 0x00);
    
    // Basic ROM structure
    mock_rom_data_[0x140145] = 0xFF; // Vanilla ASM
    
    // Tile16 expansion flag
    mock_rom_data_[0x017D28] = 0x0F; // Vanilla
    
    // Tile32 expansion flag  
    mock_rom_data_[0x01772E] = 0x04; // Vanilla
    
    // Basic map data
    for (int i = 0; i < 160; i++) {
      mock_rom_data_[0x012844 + i] = 0x00; // Small areas
    }
    
    // Setup entrance data (matches ZScream Constants.OWEntranceMap/Pos/EntranceId)
    for (int i = 0; i < 129; i++) {
      mock_rom_data_[0x0DB96F + (i * 2)] = i & 0xFF; // Map ID
      mock_rom_data_[0x0DB96F + (i * 2) + 1] = (i >> 8) & 0xFF;
      mock_rom_data_[0x0DBA71 + (i * 2)] = (i * 16) & 0xFF; // Map Position
      mock_rom_data_[0x0DBA71 + (i * 2) + 1] = ((i * 16) >> 8) & 0xFF;
      mock_rom_data_[0x0DBB73 + i] = i & 0xFF; // Entrance ID
    }
    
    // Setup exit data (matches ZScream Constants.OWExit*)
    for (int i = 0; i < 0x4F; i++) {
      mock_rom_data_[0x015D8A + (i * 2)] = i & 0xFF; // Room ID
      mock_rom_data_[0x015D8A + (i * 2) + 1] = (i >> 8) & 0xFF;
      mock_rom_data_[0x015E28 + i] = i & 0xFF; // Map ID
      mock_rom_data_[0x015E77 + (i * 2)] = i & 0xFF; // VRAM
      mock_rom_data_[0x015E77 + (i * 2) + 1] = (i >> 8) & 0xFF;
      // Add other exit fields...
    }
  }

  std::vector<uint8_t> mock_rom_data_;
  std::unique_ptr<Rom> rom_;
  std::unique_ptr<Overworld> overworld_;
  bool use_real_rom_ = false;
};

// Test Tile32 expansion detection
TEST_F(OverworldIntegrationTest, Tile32ExpansionDetection) {
  mock_rom_data_[0x01772E] = 0x04;
  mock_rom_data_[0x140145] = 0xFF;
  
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  // Test expanded detection
  mock_rom_data_[0x01772E] = 0x05;
  overworld_ = std::make_unique<Overworld>(rom_.get());
  
  status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
}

// Test Tile16 expansion detection  
TEST_F(OverworldIntegrationTest, Tile16ExpansionDetection) {
  mock_rom_data_[0x017D28] = 0x0F;
  mock_rom_data_[0x140145] = 0xFF;
  
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  // Test expanded detection
  mock_rom_data_[0x017D28] = 0x10;
  overworld_ = std::make_unique<Overworld>(rom_.get());
  
  status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
}

// Test entrance loading matches ZScream coordinate calculation
TEST_F(OverworldIntegrationTest, EntranceCoordinateCalculation) {
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  const auto& entrances = overworld_->entrances();
  EXPECT_EQ(entrances.size(), 129);
  
  // Verify coordinate calculation matches ZScream logic:
  // int p = mapPos >> 1;
  // int x = p % 64;
  // int y = p >> 6;
  // int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
  // int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
  
  for (int i = 0; i < std::min(10, static_cast<int>(entrances.size())); i++) {
    const auto& entrance = entrances[i];
    
    uint16_t map_pos = i * 16; // Our test data
    uint16_t map_id = i; // Our test data
    
    int position = map_pos >> 1;
    int x_coord = position % 64;
    int y_coord = position >> 6;
    int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
    int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
    
    EXPECT_EQ(entrance.x_, expected_x);
    EXPECT_EQ(entrance.y_, expected_y);
    EXPECT_EQ(entrance.entrance_id_, i);
    EXPECT_FALSE(entrance.is_hole_);
  }
}

// Test exit loading matches ZScream data structure
TEST_F(OverworldIntegrationTest, ExitDataLoading) {
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  const auto& exits = overworld_->exits();
  EXPECT_EQ(exits->size(), 0x4F);
  
  // Verify exit data matches our test data
  for (int i = 0; i < std::min(5, static_cast<int>(exits->size())); i++) {
    const auto& exit = exits->at(i);
    // EXPECT_EQ(exit.room_id_, i);
    // EXPECT_EQ(exit.map_id_, i);
    // EXPECT_EQ(exit.map_pos_, i);
  }
}

// Test ASM version detection affects item loading
TEST_F(OverworldIntegrationTest, ASMVersionItemLoading) {
  // Test vanilla ASM (should limit to 0x80 maps)
  mock_rom_data_[0x140145] = 0xFF;
  overworld_ = std::make_unique<Overworld>(rom_.get());
  
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  const auto& items = overworld_->all_items();
  
  // Test v3+ ASM (should support all 0xA0 maps)
  mock_rom_data_[0x140145] = 0x03;
  overworld_ = std::make_unique<Overworld>(rom_.get());
  
  status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  const auto& items_v3 = overworld_->all_items();
  // v3 should have more comprehensive support
  EXPECT_GE(items_v3.size(), items.size());
}

// Test map size assignment logic
TEST_F(OverworldIntegrationTest, MapSizeAssignment) {
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  const auto& maps = overworld_->overworld_maps();
  EXPECT_EQ(maps.size(), 160);
  
  // Verify all maps are initialized
  for (const auto& map : maps) {
    EXPECT_GE(map.area_size(), AreaSizeEnum::SmallArea);
    EXPECT_LE(map.area_size(), AreaSizeEnum::TallArea);
  }
}

// Test integration with ZSCustomOverworld version detection
TEST_F(OverworldIntegrationTest, ZSCustomOverworldVersionIntegration) {
  if (!use_real_rom_) {
    GTEST_SKIP() << "Real ROM required for ZSCustomOverworld version testing";
  }
  
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  // Check ASM version detection
  auto version_byte = rom_->ReadByte(0x140145);
  ASSERT_TRUE(version_byte.ok());
  
  uint8_t asm_version = *version_byte;
  
  if (asm_version == 0xFF) {
    // Vanilla ROM
    EXPECT_FALSE(overworld_->expanded_tile16());
    EXPECT_FALSE(overworld_->expanded_tile32());
  } else if (asm_version >= 0x02 && asm_version <= 0x03) {
    // ZSCustomOverworld v2/v3
    // Should have expanded features
    EXPECT_TRUE(overworld_->expanded_tile16());
    EXPECT_TRUE(overworld_->expanded_tile32());
  }
  
  // Verify version-specific features are properly detected
  if (asm_version >= 0x03) {
    // v3 features should be available
    const auto& maps = overworld_->overworld_maps();
    EXPECT_EQ(maps.size(), 160); // All 160 maps supported in v3
  }
}

// Test compatibility with RomDependentTestSuite infrastructure
TEST_F(OverworldIntegrationTest, RomDependentTestSuiteCompatibility) {
  if (!use_real_rom_) {
    GTEST_SKIP() << "Real ROM required for RomDependentTestSuite compatibility testing";
  }
  
  // Test that our overworld loading works with the same patterns as RomDependentTestSuite
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  // Verify ROM-dependent features work correctly
  EXPECT_TRUE(overworld_->is_loaded());
  
  const auto& maps = overworld_->overworld_maps();
  EXPECT_EQ(maps.size(), 160);
  
  // Test that we can access the same data structures as RomDependentTestSuite
  for (int i = 0; i < std::min(10, static_cast<int>(maps.size())); i++) {
    const auto& map = maps[i];
    
    // Verify map properties are accessible
    EXPECT_GE(map.area_graphics(), 0);
    EXPECT_GE(map.main_palette(), 0);
    EXPECT_GE(map.area_size(), AreaSizeEnum::SmallArea);
    EXPECT_LE(map.area_size(), AreaSizeEnum::TallArea);
  }
  
  // Test that sprite data is accessible (matches RomDependentTestSuite expectations)
  const auto& sprites = overworld_->sprites(0);
  EXPECT_EQ(sprites.size(), 3); // Three game states
  
  // Test that item data is accessible
  const auto& items = overworld_->all_items();
  EXPECT_GE(items.size(), 0);
  
  // Test that entrance/exit data is accessible
  const auto& entrances = overworld_->entrances();
  const auto& exits = overworld_->exits();
  EXPECT_EQ(entrances.size(), 129);
  EXPECT_EQ(exits->size(), 0x4F);
}

// Test comprehensive overworld data integrity
TEST_F(OverworldIntegrationTest, ComprehensiveDataIntegrity) {
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  // Verify all major data structures are properly loaded
  EXPECT_GT(overworld_->tiles16().size(), 0);
  EXPECT_GT(overworld_->tiles32_unique().size(), 0);
  
  // Verify map organization matches ZScream expectations
  const auto& map_tiles = overworld_->map_tiles();
  EXPECT_EQ(map_tiles.light_world.size(), 512);
  EXPECT_EQ(map_tiles.dark_world.size(), 512);
  EXPECT_EQ(map_tiles.special_world.size(), 512);
  
  // Verify each world has proper 512x512 tile data
  for (const auto& row : map_tiles.light_world) {
    EXPECT_EQ(row.size(), 512);
  }
  for (const auto& row : map_tiles.dark_world) {
    EXPECT_EQ(row.size(), 512);
  }
  for (const auto& row : map_tiles.special_world) {
    EXPECT_EQ(row.size(), 512);
  }
  
  // Verify overworld maps are properly initialized
  const auto& maps = overworld_->overworld_maps();
  EXPECT_EQ(maps.size(), 160);
  
  for (const auto& map : maps) {
    // TODO: Find a way to compare
    // EXPECT_TRUE(map.bitmap_data() != nullptr);
  }
  
  // Verify tile types are loaded
  const auto& tile_types = overworld_->all_tiles_types();
  EXPECT_EQ(tile_types.size(), 0x200);
}

// Test ZScream coordinate calculation compatibility
TEST_F(OverworldIntegrationTest, ZScreamCoordinateCompatibility) {
  auto status = overworld_->Load(rom_.get());
  ASSERT_TRUE(status.ok());
  
  const auto& entrances = overworld_->entrances();
  EXPECT_EQ(entrances.size(), 129);
  
  // Test coordinate calculation matches ZScream logic exactly
  for (int i = 0; i < std::min(10, static_cast<int>(entrances.size())); i++) {
    const auto& entrance = entrances[i];
    
    // ZScream coordinate calculation:
    // int p = mapPos >> 1;
    // int x = p % 64;
    // int y = p >> 6;
    // int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
    // int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
    
    uint16_t map_pos = entrance.map_pos_;
    uint16_t map_id = entrance.map_id_;
    
    int position = map_pos >> 1;
    int x_coord = position % 64;
    int y_coord = position >> 6;
    int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
    int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
    
    EXPECT_EQ(entrance.x_, expected_x);
    EXPECT_EQ(entrance.y_, expected_y);
  }
  
  // Test hole coordinate calculation with 0x400 offset
  const auto& holes = overworld_->holes();
  EXPECT_EQ(holes.size(), 0x13);
  
  for (int i = 0; i < std::min(5, static_cast<int>(holes.size())); i++) {
    const auto& hole = holes[i];
    
    // ZScream hole coordinate calculation:
    // int p = (mapPos + 0x400) >> 1;
    // int x = p % 64;
    // int y = p >> 6;
    // int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
    // int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
    
    uint16_t map_pos = hole.map_pos_;
    uint16_t map_id = hole.map_id_;
    
    int position = map_pos >> 1;
    int x_coord = position % 64;
    int y_coord = position >> 6;
    int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
    int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
    
    EXPECT_EQ(hole.x_, expected_x);
    EXPECT_EQ(hole.y_, expected_y);
    EXPECT_TRUE(hole.is_hole_);
  }
}

} // namespace zelda3
} // namespace yaze