use super::*;
use crate::{
    csharp::CSharpCodeGenerator,
    go::GoCodeGenerator,
    java::JavaCodeGenerator,
    options::{LanguageCodegenOptions, RuntimeFormat, RustCodegenOptions, RustMapType},
    rust::RustCodeGenerator,
    scala::ScalaCodeGenerator,
};
use sora_ir::{model::ConfigIr, normalize::normalize_schema};
use sora_schema::model::SchemaFile;
use std::{
    path::PathBuf,
    sync::atomic::{AtomicU64, Ordering},
};

static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);

#[test]
fn generates_rust_and_kotlin_files() {
    let ir = example_ir();
    let base = temp_dir();
    let rust_out = base.join("rust");
    let kotlin_out = base.join("kotlin");
    let kotlin_package_out = kotlin_out.join("game_config");

    RustCodeGenerator.generate(&ir, &rust_out).unwrap();
    KotlinCodeGenerator.generate(&ir, &kotlin_out).unwrap();

    let rust_item = std::fs::read_to_string(rust_out.join("item.rs")).unwrap();
    let rust_item_type = std::fs::read_to_string(rust_out.join("item_type.rs")).unwrap();
    let rust_action = std::fs::read_to_string(rust_out.join("action.rs")).unwrap();
    let rust_runtime = std::fs::read_to_string(rust_out.join("runtime.rs")).unwrap();
    let rust_mod = std::fs::read_to_string(rust_out.join("mod.rs")).unwrap();
    let kotlin_item = std::fs::read_to_string(kotlin_package_out.join("Item.kt")).unwrap();
    let kotlin_action = std::fs::read_to_string(kotlin_package_out.join("Action.kt")).unwrap();
    let kotlin_runtime = std::fs::read_to_string(kotlin_package_out.join("Runtime.kt")).unwrap();
    let kotlin_config = std::fs::read_to_string(kotlin_package_out.join("SoraConfig.kt")).unwrap();

    assert!(rust_item.starts_with("// This file was generated by Sora. Do not edit manually."));
    assert!(rust_item.contains("pub struct Item"));
    assert!(rust_item.contains("/// Item id"));
    assert!(rust_item.contains("pub item_type: ItemType"));
    assert!(rust_item.contains("pub action: Action"));
    assert!(rust_item.contains("pub tags: std::collections::HashSet<String>"));
    assert!(rust_item.contains("pub weights: std::collections::HashMap<String, i32>"));
    assert!(rust_item.contains("impl super::runtime::SoraDecode for Item"));
    assert!(!rust_item.contains("impl std::fmt::Display for Item"));
    assert!(!rust_item_type.contains("impl std::fmt::Display for ItemType"));
    assert!(rust_action.contains("pub enum Action"));
    assert!(rust_action.contains("AddItem {"));
    assert!(rust_action.contains("impl super::runtime::SoraDecode for Action"));
    assert!(!rust_action.contains("impl std::fmt::Display for Action"));
    assert!(rust_runtime.contains("pub struct SoraBundle"));
    assert!(rust_runtime.contains("pub trait SoraTableSource"));
    assert!(rust_runtime.contains(
        "impl<T: SoraDecode + Eq + std::hash::Hash> SoraDecode for std::collections::HashSet<T>"
    ));
    assert!(rust_runtime.contains("impl<K, V> SoraDecode for std::collections::HashMap<K, V>"));
    assert!(rust_mod.contains("pub struct SoraConfig"));
    assert!(rust_mod.contains("from_source"));
    assert!(!rust_mod.contains("from_bytes"));
    assert!(rust_mod.contains("pub type SoraMap<K, V> = std::collections::HashMap<K, V>;"));
    assert!(rust_mod.contains("pub trait ErasedSoraTable: std::any::Any + Send + Sync"));
    assert!(rust_mod.contains("pub trait SoraKeyedTable: ErasedSoraTable"));
    assert!(rust_mod.contains("tables: SoraMap<&'static str, Box<dyn ErasedSoraTable>>"));
    assert!(
        rust_mod.contains("pub fn tables(&self) -> impl Iterator<Item = &dyn ErasedSoraTable>")
    );
    assert!(rust_item.contains("pub const NAME: &'static str = \"Item\";"));
    assert!(rust_mod.contains("source.decode_table::<item::Item>(item::ItemTable::NAME)?"));
    assert!(rust_item.contains("pub struct ItemTable"));
    assert!(rust_item.contains("rows: SoraMap<i32, Item>"));
    assert!(rust_item.contains("keys: Vec<i32>"));
    assert_contains_before(&rust_item, "keys: Vec<i32>", "rows: SoraMap<i32, Item>");
    assert!(rust_item.contains("by_name: SoraMap<String, i32>"));
    assert!(rust_item.contains("by_item_type: SoraMap<ItemType, Vec<i32>>"));
    assert!(rust_item.contains("pub const INFO: super::SoraTableInfo"));
    assert!(rust_item.contains("shape: super::SoraTableShape::Keyed"));
    assert!(rust_item.contains("primary_key: Some(super::SoraKeyInfo"));
    assert!(rust_item.contains("impl super::ErasedSoraTable for ItemTable"));
    assert!(rust_item.contains("impl super::SoraKeyedTable for ItemTable"));
    assert!(rust_item.contains("impl std::ops::Deref for ItemTable"));
    assert!(rust_item.contains("|row| row.id"));
    assert!(rust_item.contains("|row| row.name.clone()"));
    assert!(rust_item.contains("|row| row.item_type"));
    assert!(
        rust_mod
            .contains("fn table<T: ErasedSoraTable + 'static>(&self, name: &'static str) -> &T")
    );
    assert!(!rust_mod.contains("as_any"));
    assert!(rust_mod.contains("let table: &dyn std::any::Any = table.as_ref();"));
    assert!(rust_mod.contains("table.downcast_ref::<T>()"));
    assert!(rust_mod.contains("pub fn item(&self) -> &item::ItemTable"));
    assert!(rust_item.contains("pub fn get(&self, key: &i32) -> Option<&Item>"));
    assert!(rust_item.contains("pub fn keys(&self) -> &[i32]"));
    assert!(rust_item.contains("pub fn ordered_rows(&self) -> impl Iterator<Item = &Item>"));
    assert!(rust_item.contains("pub fn get_by_name(&self, name: &str) -> Option<&Item>"));
    assert!(rust_item.contains(
        "pub fn find_by_item_type(&self, item_type: ItemType) -> impl Iterator<Item = &Item>"
    ));
    assert!(!rust_mod.contains("pub fn get_item"));
    assert!(!rust_mod.contains("pub fn iter_item"));
    assert!(!rust_mod.contains("decode_singleton_table"));
    assert!(kotlin_item.starts_with("// This file was generated by Sora. Do not edit manually."));
    assert!(kotlin_item.contains("data class Item"));
    assert!(kotlin_item.contains("package game_config"));
    assert!(kotlin_item.contains("/** Item id */"));
    assert!(kotlin_item.contains("val itemType: ItemType"));
    assert!(kotlin_item.contains("val action: Action"));
    assert!(kotlin_item.contains("val tags: List<String>"));
    assert!(kotlin_item.contains("val weights: Map<String, Int>"));
    assert!(kotlin_item.contains("reader.readMap({ reader.readString() }, { reader.readI32() })"));
    assert!(kotlin_item.contains(
        "obj.get(\"weights\").asMap({ item -> item.asString() }, { item -> item.asInt() })"
    ));
    assert!(kotlin_action.contains("sealed class Action"));
    assert!(kotlin_action.contains("data class AddItem"));
    assert!(kotlin_action.contains("fun decode(reader: SoraReader): Action"));
    assert!(kotlin_item.contains("fun decode(reader: SoraReader): Item"));
    assert!(kotlin_item.contains("fun decode(value: SoraValue): Item"));
    assert!(kotlin_runtime.contains("class SoraBundle"));
    assert!(kotlin_runtime.contains("interface SoraTableSource"));
    assert!(kotlin_config.contains("interface SoraTable<out R : Any>"));
    assert!(kotlin_config.contains("interface SoraKeyedTable<K : Any, R : Any>"));
    assert!(kotlin_config.contains("class SoraConfig private constructor"));
    assert!(kotlin_config.contains("private val tableMap: Map<String, SoraTable<*>>"));
    assert!(kotlin_config.contains("val item: ItemTable"));
    assert!(kotlin_config.contains("get() = table(ItemTable.NAME)"));
    assert!(kotlin_config.contains("fun fromSource(source: SoraTableSource): SoraConfig"));
    assert!(!kotlin_config.contains("fromBytes"));
    assert!(kotlin_config.contains("tables[ItemTable.NAME] = ItemTable.decode(source)"));
    assert!(kotlin_item.contains("class ItemTable private constructor"));
    assert!(kotlin_item.contains("const val NAME: String = \"Item\""));
    assert!(kotlin_item.contains("val rows: Map<Int, Item>"));
    assert!(kotlin_item.contains("val orderedKeys: List<Int>"));
    assert!(kotlin_item.contains(") : SoraKeyedTable<Int, Item>, Map<Int, Item> by rows"));
    assert_contains_before(
        &kotlin_item,
        "val orderedKeys: List<Int>",
        "val rows: Map<Int, Item>",
    );
    assert!(kotlin_item.contains("private val nameIndex: Map<String, Item>"));
    assert!(kotlin_item.contains("private val itemTypeIndex: Map<ItemType, List<Item>>"));
    assert!(
        kotlin_item
            .contains("fun orderedValues(): List<Item> = orderedKeys.mapNotNull { rows[it] }")
    );
    assert!(kotlin_item.contains("fun getByName(name: String): Item? = nameIndex[name]"));
    assert!(kotlin_item.contains(
        "fun findByItemType(itemType: ItemType): List<Item> = itemTypeIndex[itemType].orEmpty()"
    ));

    let _ = std::fs::remove_dir_all(base);
}

#[test]
fn kotlin_files_are_written_under_package_path() {
    let mut ir = example_ir();
    ir.package = "com.sora.game_config".to_owned();
    let base = temp_dir();
    let kotlin_out = base.join("kotlin");

    KotlinCodeGenerator.generate(&ir, &kotlin_out).unwrap();

    let item = std::fs::read_to_string(kotlin_out.join("com/sora/game_config/Item.kt")).unwrap();
    assert!(item.contains("package com.sora.game_config"));
    assert!(!kotlin_out.join("Item.kt").exists());

    let _ = std::fs::remove_dir_all(base);
}

#[test]
fn kotlin_package_path_rejects_invalid_segments() {
    let mut ir = example_ir();
    ir.package = "com.sora-game".to_owned();
    let base = temp_dir();

    let error = KotlinCodeGenerator
        .generate(&ir, &base.join("kotlin"))
        .unwrap_err();

    assert!(
        error
            .to_string()
            .contains("dot-separated identifier segments")
    );

    let _ = std::fs::remove_dir_all(base);
}

#[test]
fn rust_config_api_respects_table_modes() {
    let ir = table_mode_ir();
    let base = temp_dir();
    let rust_out = base.join("rust");

    RustCodeGenerator.generate(&ir, &rust_out).unwrap();

    let rust_mod = std::fs::read_to_string(rust_out.join("mod.rs")).unwrap();
    let rust_item = std::fs::read_to_string(rust_out.join("item.rs")).unwrap();
    let rust_settings = std::fs::read_to_string(rust_out.join("settings.rs")).unwrap();
    assert!(rust_item.contains("pub struct ItemTable"));
    assert!(rust_item.contains("rows: SoraMap<i32, Item>"));
    assert!(rust_settings.contains("pub struct SettingsTable"));
    assert!(rust_settings.contains("rows: Settings"));
    assert!(rust_mod.contains("pub fn item(&self) -> &item::ItemTable"));
    assert!(rust_mod.contains("pub fn settings(&self) -> &settings::SettingsTable"));
    assert!(!rust_mod.contains("pub fn settings_row"));
    assert!(rust_mod.contains("decode_singleton_table"));

    let _ = std::fs::remove_dir_all(base);
}

#[test]
fn kotlin_list_tables_extend_abstract_list() {
    let ir = table_mode_ir();
    let base = temp_dir();
    let kotlin_out = base.join("kotlin");

    KotlinCodeGenerator.generate(&ir, &kotlin_out).unwrap();

    let package_out = kotlin_out.join("game_config");
    let reward = std::fs::read_to_string(package_out.join("Reward.kt")).unwrap();
    assert!(reward.contains("class RewardTable private constructor"));
    assert!(reward.contains(") : AbstractList<Reward>(), SoraListTable<Reward>"));
    assert!(reward.contains("override fun get(index: Int): Reward = rows[index]"));
    assert!(!reward.contains("List<Reward> by rows"));

    let _ = std::fs::remove_dir_all(base);
}

#[test]
fn rust_config_api_can_use_fx_hash_map() {
    let ir = example_ir();
    let base = temp_dir();
    let rust_out = base.join("rust");

    RustCodeGenerator
        .generate_with_options(
            &ir,
            RustCodegenOptions {
                map_type: RustMapType::FxHashMap,
                ..Default::default()
            },
            &rust_out,
        )
        .unwrap();

    let rust_mod = std::fs::read_to_string(rust_out.join("mod.rs")).unwrap();
    let rust_item = std::fs::read_to_string(rust_out.join("item.rs")).unwrap();
    assert!(rust_mod.contains("pub type SoraMap<K, V> = rustc_hash::FxHashMap<K, V>;"));
    assert!(rust_item.contains("pub struct ItemTable"));
    assert!(rust_item.contains("rows: SoraMap<i32, Item>"));
    assert!(rust_mod.contains("tables: SoraMap<&'static str, Box<dyn ErasedSoraTable>>"));

    let _ = std::fs::remove_dir_all(base);
}

#[test]
fn csharp_supports_export_runtime_formats() {
    for (runtime_format, parse_function) in [
        (RuntimeFormat::Json, "ParseJson"),
        (RuntimeFormat::Cbor, "ParseCbor"),
        (RuntimeFormat::SoraProtobuf, "ParseProtobuf"),
    ] {
        let ir = example_ir();
        let base = temp_dir();
        let csharp_out = base.join("csharp");

        CSharpCodeGenerator
            .generate_with_options(&ir, LanguageCodegenOptions { runtime_format }, &csharp_out)
            .unwrap();

        let runtime = std::fs::read_to_string(csharp_out.join("Runtime.cs")).unwrap();
        let config = std::fs::read_to_string(csharp_out.join("SoraConfig.cs")).unwrap();
        let item = std::fs::read_to_string(csharp_out.join("Item.cs")).unwrap();
        let action = std::fs::read_to_string(csharp_out.join("Action.cs")).unwrap();

        assert!(runtime.contains("public interface ISoraTableSource"));
        assert!(runtime.contains("public sealed class SoraValueBundle"));
        assert!(runtime.contains(parse_function));
        assert!(!config.contains(&format!("SoraValueBundle.{parse_function}(bytes)")));
        assert!(config.contains("FromSource(ISoraTableSource source)"));
        assert!(item.contains("internal static Item Decode(SoraValue value)"));
        assert!(item.contains("obj.Get(\"id\").AsInt32()"));
        assert!(item.contains("ItemTypeCodec.Decode(obj.Get(\"item_type\"))"));
        assert!(action.contains("internal static Action Decode(SoraValue value)"));

        let _ = std::fs::remove_dir_all(base);
    }
}

#[test]
fn kotlin_supports_export_runtime_formats() {
    for (runtime_format, parse_function) in [
        (RuntimeFormat::Json, "parseJson"),
        (RuntimeFormat::Cbor, "parseCbor"),
        (RuntimeFormat::SoraProtobuf, "parseProtobuf"),
    ] {
        let ir = example_ir();
        let base = temp_dir();
        let kotlin_out = base.join("kotlin");

        KotlinCodeGenerator
            .generate_with_options(&ir, LanguageCodegenOptions { runtime_format }, &kotlin_out)
            .unwrap();

        let package_out = kotlin_out.join("game_config");
        let runtime = std::fs::read_to_string(package_out.join("Runtime.kt")).unwrap();
        let config = std::fs::read_to_string(package_out.join("SoraConfig.kt")).unwrap();
        let item = std::fs::read_to_string(package_out.join("Item.kt")).unwrap();
        let action = std::fs::read_to_string(package_out.join("Action.kt")).unwrap();

        assert!(runtime.contains("class SoraValueBundle"));
        assert!(runtime.contains(parse_function));
        assert!(!config.contains(&format!("SoraValueBundle.{parse_function}(bytes)")));
        assert!(config.contains("fun fromSource(source: SoraTableSource): SoraConfig"));
        assert!(item.contains("fun decode(value: SoraValue): Item"));
        assert!(item.contains("obj.get(\"id\").asInt()"));
        assert!(item.contains("ItemType.decode(obj.get(\"item_type\"))"));
        assert!(action.contains("fun decode(value: SoraValue): Action"));

        let _ = std::fs::remove_dir_all(base);
    }
}

#[test]
fn go_supports_export_runtime_formats() {
    for (runtime_format, parse_function) in [
        (RuntimeFormat::Json, "ParseJsonBundle"),
        (RuntimeFormat::Cbor, "ParseCborBundle"),
        (RuntimeFormat::SoraProtobuf, "ParseProtobufBundle"),
    ] {
        let ir = example_ir();
        let base = temp_dir();
        let go_out = base.join("go");

        GoCodeGenerator
            .generate_with_options(&ir, LanguageCodegenOptions { runtime_format }, &go_out)
            .unwrap();

        let runtime = std::fs::read_to_string(go_out.join("runtime.go")).unwrap();
        let config = std::fs::read_to_string(go_out.join("config.go")).unwrap();
        let item = std::fs::read_to_string(go_out.join("item.go")).unwrap();
        let action = std::fs::read_to_string(go_out.join("action.go")).unwrap();

        assert!(runtime.contains("type SoraValueBundle struct"));
        assert!(runtime.contains(parse_function));
        assert!(!config.contains(&format!("{parse_function}(bytes)")));
        assert!(runtime.contains("type SoraTableSource interface"));
        assert!(config.contains("func NewSoraConfigFromSource(source SoraTableSource)"));
        assert!(item.contains("func decodeItemValue(input SoraValue) (Item, error)"));
        assert!(item.contains("obj.Get(\"id\").AsInt32()"));
        assert!(item.contains("decodeItemTypeValue(obj.Get(\"item_type\"))"));
        assert!(action.contains("func decodeActionValue(input SoraValue) (Action, error)"));

        let _ = std::fs::remove_dir_all(base);
    }
}

#[test]
fn java_supports_export_runtime_formats() {
    for (runtime_format, parse_function) in [
        (RuntimeFormat::Json, "parseJson"),
        (RuntimeFormat::Cbor, "parseCbor"),
        (RuntimeFormat::SoraProtobuf, "parseProtobuf"),
    ] {
        let ir = example_ir();
        let base = temp_dir();
        let java_out = base.join("java");

        JavaCodeGenerator
            .generate_with_options(&ir, LanguageCodegenOptions { runtime_format }, &java_out)
            .unwrap();

        let package_out = java_out.join("game_config");
        let runtime = std::fs::read_to_string(package_out.join("Runtime.java")).unwrap();
        let config = std::fs::read_to_string(package_out.join("SoraConfig.java")).unwrap();
        let item = std::fs::read_to_string(package_out.join("Item.java")).unwrap();
        let action = std::fs::read_to_string(package_out.join("Action.java")).unwrap();

        assert!(runtime.contains("final class SoraValueBundle"));
        assert!(runtime.contains(parse_function));
        assert!(!config.contains(&format!("SoraValueBundle.{parse_function}(bytes)")));
        assert!(runtime.contains("interface SoraTableSource"));
        assert!(config.contains("fromSource(SoraTableSource source)"));
        assert!(item.contains("static Item decode(SoraValue value)"));
        assert!(item.contains("obj.get(\"id\").asInt()"));
        assert!(item.contains("ItemType.decode(obj.get(\"item_type\"))"));
        assert!(action.contains("static Action decode(SoraValue value)"));

        let _ = std::fs::remove_dir_all(base);
    }
}

#[test]
fn scala_supports_export_runtime_formats() {
    for (runtime_format, parse_function) in [
        (RuntimeFormat::Json, "parseJson"),
        (RuntimeFormat::Cbor, "parseCbor"),
        (RuntimeFormat::SoraProtobuf, "parseProtobuf"),
    ] {
        let ir = example_ir();
        let base = temp_dir();
        let scala_out = base.join("scala");

        ScalaCodeGenerator
            .generate_with_options(
                &ir,
                crate::options::ScalaCodegenOptions {
                    runtime_format,
                    ..Default::default()
                },
                &scala_out,
            )
            .unwrap();

        let package_out = scala_out.join("game_config");
        let runtime = std::fs::read_to_string(package_out.join("SoraRuntime.scala")).unwrap();
        let config = std::fs::read_to_string(package_out.join("SoraConfig.scala")).unwrap();
        let item = std::fs::read_to_string(package_out.join("Item.scala")).unwrap();
        let action = std::fs::read_to_string(package_out.join("Action.scala")).unwrap();

        assert!(runtime.contains("final class SoraValueBundle"));
        assert!(runtime.contains(parse_function));
        assert!(!config.contains(&format!("SoraValueBundle.{parse_function}(bytes)")));
        assert!(config.contains("def fromSource(source: SoraTableSource): SoraConfig"));
        assert!(item.contains("def decode(value: SoraValue): Item"));
        assert!(item.contains("obj.get(\"id\").asInt"));
        assert!(item.contains("ItemType.decode(obj.get(\"item_type\"))"));
        assert!(action.contains("def decode(value: SoraValue): Action"));

        let _ = std::fs::remove_dir_all(base);
    }
}

#[test]
fn generates_csharp_java_and_go_files() {
    let mut ir = example_ir();
    ir.package = "com.sora.game_config".to_owned();
    let base = temp_dir();
    let csharp_out = base.join("csharp");
    let java_out = base.join("java");
    let go_out = base.join("go");

    CSharpCodeGenerator.generate(&ir, &csharp_out).unwrap();
    JavaCodeGenerator.generate(&ir, &java_out).unwrap();
    GoCodeGenerator.generate(&ir, &go_out).unwrap();

    let csharp_item = std::fs::read_to_string(csharp_out.join("Item.cs")).unwrap();
    let csharp_config = std::fs::read_to_string(csharp_out.join("SoraConfig.cs")).unwrap();
    let java_item =
        std::fs::read_to_string(java_out.join("com/sora/game_config/Item.java")).unwrap();
    let java_config =
        std::fs::read_to_string(java_out.join("com/sora/game_config/SoraConfig.java")).unwrap();
    let go_item = std::fs::read_to_string(go_out.join("item.go")).unwrap();
    let go_config = std::fs::read_to_string(go_out.join("config.go")).unwrap();

    assert!(csharp_item.starts_with("// This file was generated by Sora. Do not edit manually."));
    assert!(csharp_item.contains("namespace com.sora.game_config;"));
    assert!(csharp_item.contains("public sealed record Item"));
    assert!(csharp_item.contains("// Item id"));
    assert!(csharp_item.contains("List<string> Tags"));
    assert!(csharp_item.contains("Dictionary<string, int> Weights"));
    assert!(
        csharp_item.contains("reader.ReadMap(() => reader.ReadString(), () => reader.ReadInt32())")
    );
    assert!(
        csharp_item.contains(
            "obj.Get(\"weights\").AsMap(item => item.AsString(), item => item.AsInt32())"
        )
    );
    assert!(csharp_item.contains("public sealed class ItemTable : ISoraKeyedTable<int, Item>"));
    assert!(csharp_item.contains("Dictionary<int, Item>"));
    assert!(csharp_item.contains("private readonly List<int> keys"));
    assert_contains_before(
        &csharp_item,
        "private readonly List<int> keys",
        "private readonly Dictionary<int, Item> rows",
    );
    assert!(csharp_item.contains("private readonly Dictionary<string, Item> byName"));
    assert!(csharp_item.contains("private readonly Dictionary<ItemType, List<Item>> byItemType"));
    assert!(csharp_item.contains("public Item? GetByName(string name)"));
    assert!(csharp_item.contains("public IReadOnlyList<int> OrderedKeys => keys"));
    assert!(csharp_item.contains("public IEnumerable<int> Keys => keys"));
    assert!(csharp_item.contains("public IEnumerator<KeyValuePair<int, Item>> GetEnumerator()"));
    assert!(csharp_item.contains("public IReadOnlyList<Item> FindByItemType(ItemType itemType)"));
    assert!(csharp_item.contains("public const string TableName = \"Item\";"));
    assert!(csharp_item.contains("public static readonly SoraTableInfo TableInfo"));
    assert!(csharp_config.contains("public sealed class SoraConfig"));
    assert!(!csharp_config.contains("public sealed class ItemTable"));
    assert!(
        csharp_config.contains("public ItemTable Item => Table<ItemTable>(ItemTable.TableName)")
    );
    assert!(java_item.starts_with("// This file was generated by Sora. Do not edit manually."));
    assert!(java_item.contains("package com.sora.game_config;"));
    assert!(java_item.contains("public final class Item"));
    assert!(java_item.contains("/** Item id */"));
    assert!(java_item.contains("java.util.List<String> tags"));
    assert!(java_item.contains("java.util.Map<String, Integer> weights"));
    assert!(
        java_item.contains("reader.readMap(() -> reader.readString(), () -> reader.readI32())")
    );
    assert!(
        java_item
            .contains("obj.get(\"weights\").asMap(item -> item.asString(), item -> item.asInt())")
    );
    assert!(java_item.contains(
        "final class ItemTable extends java.util.AbstractMap<Integer, Item> implements SoraKeyedTable<Integer, Item>"
    ));
    assert!(java_item.contains("static final String NAME = \"Item\";"));
    assert!(java_item.contains("static final SoraTableInfo INFO"));
    assert!(java_item.contains("java.util.Map<Integer, Item>"));
    assert!(java_item.contains("private final List<Integer> keys"));
    assert_contains_before(
        &java_item,
        "private final List<Integer> keys",
        "private final java.util.Map<Integer, Item> rows",
    );
    assert!(java_item.contains("private final Map<String, Item> byName"));
    assert!(java_item.contains("private final Map<ItemType, List<Item>> byItemType"));
    assert!(java_item.contains("public Item getByName(String name)"));
    assert!(java_item.contains("public List<Integer> orderedKeys()"));
    assert!(java_item.contains("public java.util.Set<Map.Entry<Integer, Item>> entrySet()"));
    assert!(java_item.contains("public List<Item> findByItemType(ItemType itemType)"));
    assert!(java_config.contains("public final class SoraConfig"));
    assert!(!java_config.contains("final class ItemTable extends java.util.AbstractMap"));
    assert!(java_config.contains("public ItemTable item()"));
    assert!(java_config.contains("return table(ItemTable.NAME, ItemTable.class);"));
    assert!(go_item.starts_with("// This file was generated by Sora. Do not edit manually."));
    assert!(go_item.contains("package game_config"));
    assert!(go_item.contains("type Item struct"));
    assert!(go_item.contains("// Id: Item id"));
    assert!(go_item.contains("Tags []string"));
    assert!(go_item.contains("Weights map[string]int32"));
    assert!(go_item.contains("ReadMap(reader, func(reader *SoraReader) (string, error) { return reader.ReadString() }, func(reader *SoraReader) (int32, error) { return reader.ReadInt32() })"));
    assert!(go_item.contains("DecodeSoraValueMap(obj.Get(\"weights\"), func(item SoraValue) (string, error) { return item.AsString() }, func(item SoraValue) (int32, error) { return item.AsInt32() })"));
    assert!(go_item.contains("type ItemTable struct"));
    assert!(go_item.contains("const itemTableName = \"Item\""));
    assert!(go_item.contains("var itemTableInfo = SoraTableInfo"));
    assert!(go_item.contains("map[int32]Item"));
    assert!(go_item.contains("keys []int32"));
    assert_contains_before(&go_item, "keys []int32", "rows map[int32]Item");
    assert!(go_item.contains("byName map[string]Item"));
    assert!(go_item.contains("byItemType map[ItemType][]Item"));
    assert!(go_item.contains("func (table *ItemTable) GetByName(name string) (Item, bool)"));
    assert!(go_item.contains("func (table *ItemTable) Keys() []int32"));
    assert!(go_item.contains("func (table *ItemTable) FindByItemType(itemType ItemType) []Item"));
    assert!(go_config.contains("type SoraConfig struct"));
    assert!(!go_config.contains("type ItemTable struct"));
    assert!(go_config.contains("tables[itemTableName] = itemTable"));
    assert!(go_config.contains("func NewSoraConfigFromSource(source SoraTableSource)"));
    assert!(go_config.contains("func (config *SoraConfig) Item() *ItemTable"));

    let _ = std::fs::remove_dir_all(base);
}

fn example_ir() -> ConfigIr {
    let schema: SchemaFile = toml::from_str(
        r#"
package = "game_config"

[[enums]]
name = "ItemType"
values = ["Weapon", "Armor", "Material", "Consumable"]

[[unions]]
name = "Action"

[[unions.variants]]
name = "AddItem"

[[unions.variants.fields]]
name = "item_id"
type = "i32"

[[tables]]
name = "Item"
mode = "map"
key = "id"

[[tables.fields]]
name = "id"
type = "i32"
comment = "Item id"

[[tables.fields]]
name = "name"
type = "string"
comment = "Item name"

[[tables.fields]]
name = "item_type"
type = "enum<ItemType>"
comment = "Item type"

[[tables.fields]]
name = "action"
type = "union<Action>"
comment = "Action"

[[tables.fields]]
name = "tags"
type = "set<string>"
comment = "Tags"

[[tables.fields]]
name = "weights"
type = "map<string,i32>"
comment = "Weights"

[[tables.indexes]]
name = "by_name"
fields = ["name"]
unique = true

[[tables.indexes]]
name = "by_item_type"
fields = ["item_type"]
"#,
    )
    .unwrap();

    normalize_schema(schema).unwrap()
}

fn table_mode_ir() -> ConfigIr {
    let schema: SchemaFile = toml::from_str(
        r#"
package = "game_config"

[[tables]]
name = "Item"
mode = "map"
key = "id"

[[tables.fields]]
name = "id"
type = "i32"

[[tables]]
name = "Settings"
mode = "singleton"

[[tables.fields]]
name = "version"
type = "string"

[[tables]]
name = "Reward"
mode = "list"

[[tables.fields]]
name = "id"
type = "i32"
"#,
    )
    .unwrap();

    normalize_schema(schema).unwrap()
}

fn temp_dir() -> PathBuf {
    let unique = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
    std::env::temp_dir().join(format!("sora-codegen-test-{unique}"))
}

fn assert_contains_before(haystack: &str, first: &str, second: &str) {
    let first_index = haystack
        .find(first)
        .unwrap_or_else(|| panic!("expected output to contain `{first}`"));
    let second_index = haystack
        .find(second)
        .unwrap_or_else(|| panic!("expected output to contain `{second}`"));
    assert!(
        first_index < second_index,
        "expected `{first}` to appear before `{second}`"
    );
}
