Skip to content
use duckdb::{Connection, params};
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Mutex;

use super::{ColumnInfo, TablePreview};
use crate::models::{Pokemon, PokemonMove};

#[derive(Clone)]
pub struct DuckDb {
    conn: Arc<Mutex<Connection>>,
    db_path: String,
}

impl DuckDb {
    pub fn new(db_path: &str) -> Result<Self, String> {
        let conn = if db_path == ":memory:" {
            Connection::open_in_memory()
                .map_err(|e| format!("Failed to open in-memory DuckDB: {e}"))?
        } else {
            let path = Path::new(db_path);
            if let Some(parent) = path
                .parent()
                .filter(|p| !p.as_os_str().is_empty() && !p.exists())
            {
                let _ = std::fs::create_dir_all(parent);
            }
            Connection::open(db_path)
                .map_err(|e| format!("Failed to open DuckDB file at {db_path}: {e}"))?
        };

        Ok(Self {
            conn: Arc::new(Mutex::new(conn)),
            db_path: db_path.to_string(),
        })
    }

    #[must_use]
    pub fn db_path(&self) -> &str {
        &self.db_path
    }

    pub async fn init(&self) -> Result<(), String> {
        let conn = self.conn.lock().await;

        conn.execute(
            "CREATE TABLE IF NOT EXISTS pokemon (
                id INTEGER PRIMARY KEY,
                name VARCHAR NOT NULL,
                display_name VARCHAR NOT NULL,
                types VARCHAR NOT NULL,
                hp INTEGER NOT NULL,
                attack INTEGER NOT NULL,
                defense INTEGER NOT NULL,
                sp_attack INTEGER NOT NULL,
                sp_defense INTEGER NOT NULL,
                speed INTEGER NOT NULL,
                height INTEGER NOT NULL,
                weight INTEGER NOT NULL,
                sprite_url VARCHAR NOT NULL,
                artwork_url VARCHAR NOT NULL,
                genus VARCHAR NOT NULL,
                description VARCHAR NOT NULL,
                moves_json VARCHAR NOT NULL,
                cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );",
            [],
        )
        .map_err(|e| format!("Failed to create pokemon table in DuckDB: {e}"))?;

        // Also create a pokedex_meta table to track sync state
        conn.execute(
            "CREATE TABLE IF NOT EXISTS pokedex_meta (
                key VARCHAR PRIMARY KEY,
                value VARCHAR NOT NULL,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );",
            [],
        )
        .map_err(|e| format!("Failed to create pokedex_meta table in DuckDB: {e}"))?;

        log::info!("DuckDB schema initialized successfully");
        Ok(())
    }

    pub async fn existing_pokemon_ids(&self) -> Result<std::collections::HashSet<i32>, String> {
        let conn = self.conn.lock().await;
        let mut stmt = conn
            .prepare("SELECT id FROM pokemon;")
            .map_err(|e| format!("Failed to query pokemon ids: {e}"))?;

        let rows = stmt
            .query_map([], |row| row.get::<_, i32>(0))
            .map_err(|e| format!("Failed to fetch pokemon ids: {e}"))?;

        let mut ids = std::collections::HashSet::new();
        for id in rows.flatten() {
            ids.insert(id);
        }
        Ok(ids)
    }

    pub async fn save_pokemons(&self, pokemons: &[Pokemon]) -> Result<usize, String> {
        if pokemons.is_empty() {
            return Ok(0);
        }

        let conn = self.conn.lock().await;
        conn.execute("BEGIN TRANSACTION;", [])
            .map_err(|e| format!("Failed to begin transaction: {e}"))?;

        let mut stmt = conn
            .prepare(
                "INSERT INTO pokemon (
                    id, name, display_name, types, hp, attack, defense,
                    sp_attack, sp_defense, speed, height, weight,
                    sprite_url, artwork_url, genus, description, moves_json
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                ON CONFLICT (id) DO UPDATE SET
                    name = excluded.name,
                    display_name = excluded.display_name,
                    types = excluded.types,
                    hp = excluded.hp,
                    attack = excluded.attack,
                    defense = excluded.defense,
                    sp_attack = excluded.sp_attack,
                    sp_defense = excluded.sp_defense,
                    speed = excluded.speed,
                    height = excluded.height,
                    weight = excluded.weight,
                    sprite_url = excluded.sprite_url,
                    artwork_url = excluded.artwork_url,
                    genus = excluded.genus,
                    description = excluded.description,
                    moves_json = excluded.moves_json,
                    cached_at = now();",
            )
            .map_err(|e| format!("Failed to prepare insert statement: {e}"))?;

        let mut inserted = 0;
        for p in pokemons {
            let types_str = serde_json::to_string(&p.types)
                .map_err(|e| format!("Failed to serialize types: {e}"))?;
            let moves_str = serde_json::to_string(&p.moves)
                .map_err(|e| format!("Failed to serialize moves: {e}"))?;

            stmt.execute(params![
                p.id,
                p.name,
                p.display_name,
                types_str,
                p.hp,
                p.attack,
                p.defense,
                p.sp_attack,
                p.sp_defense,
                p.speed,
                p.height,
                p.weight,
                p.sprite_url,
                p.artwork_url,
                p.genus,
                p.description,
                moves_str,
            ])
            .map_err(|e| format!("Failed to insert pokemon {}: {e}", p.id))?;

            inserted += 1;
        }

        conn.execute("COMMIT;", [])
            .map_err(|e| format!("Failed to commit transaction: {e}"))?;

        Ok(inserted)
    }

    pub async fn seed_if_empty(&self) -> Result<usize, String> {
        let count = self.table_row_count("pokemon").await?;
        if count >= 151 {
            log::info!("DuckDB pokemon table already has {count} entries; skipping seed");
            return Ok(usize::try_from(count).unwrap_or(0));
        }

        let existing_ids = self.existing_pokemon_ids().await?;
        let missing_ids: Vec<i32> = (1..=151).filter(|id| !existing_ids.contains(id)).collect();

        if missing_ids.is_empty() {
            log::info!("All 151 Pokémon already present in DuckDB; skipping seed");
            return Ok(usize::try_from(count).unwrap_or(0));
        }

        log::info!(
            "Seeding DuckDB with {} missing Gen 1 Pokémon from PokéAPI...",
            missing_ids.len()
        );
        let fetched = crate::pokeapi::fetch_gen1_pokemon_concurrent(missing_ids, 8).await?;
        let inserted = self.save_pokemons(&fetched).await?;
        log::info!("Successfully cached {inserted} Pokémon from PokéAPI into DuckDB");

        let total = self.table_row_count("pokemon").await?;
        Ok(usize::try_from(total).unwrap_or(inserted))
    }

    #[allow(dead_code, clippy::too_many_lines)]
    pub async fn seed_test_data(&self) -> Result<usize, String> {
        let sample = vec![
            Pokemon {
                id: 1,
                name: "bulbasaur".to_string(),
                display_name: "Bulbasaur".to_string(),
                types: vec!["grass".to_string(), "poison".to_string()],
                hp: 45,
                attack: 49,
                defense: 49,
                sp_attack: 65,
                sp_defense: 65,
                speed: 45,
                height: 7,
                weight: 69,
                sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png".to_string(),
                artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png".to_string(),
                genus: "Seed Pokémon".to_string(),
                description: "There is a plant seed on its back right from the day this POKéMON is born. The seed slowly grows larger.".to_string(),
                moves: vec![
                    PokemonMove {
                        name: "tackle".to_string(),
                        level: 1,
                        method: "level-up".to_string(),
                    },
                    PokemonMove {
                        name: "growl".to_string(),
                        level: 4,
                        method: "level-up".to_string(),
                    },
                ],
            },
            Pokemon {
                id: 2,
                name: "ivysaur".to_string(),
                display_name: "Ivysaur".to_string(),
                types: vec!["grass".to_string(), "poison".to_string()],
                hp: 60,
                attack: 62,
                defense: 63,
                sp_attack: 80,
                sp_defense: 80,
                speed: 60,
                height: 10,
                weight: 130,
                sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png".to_string(),
                artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/2.png".to_string(),
                genus: "Seed Pokémon".to_string(),
                description: "When the bulb on its back grows large, it appears to lose the ability to stand on its hind legs.".to_string(),
                moves: vec![
                    PokemonMove {
                        name: "tackle".to_string(),
                        level: 1,
                        method: "level-up".to_string(),
                    },
                ],
            },
            Pokemon {
                id: 3,
                name: "venusaur".to_string(),
                display_name: "Venusaur".to_string(),
                types: vec!["grass".to_string(), "poison".to_string()],
                hp: 80,
                attack: 82,
                defense: 83,
                sp_attack: 100,
                sp_defense: 100,
                speed: 80,
                height: 20,
                weight: 1000,
                sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/3.png".to_string(),
                artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/3.png".to_string(),
                genus: "Seed Pokémon".to_string(),
                description: "The plant blooms when it is absorbing solar energy. It stays on the move to seek sunlight.".to_string(),
                moves: vec![
                    PokemonMove {
                        name: "tackle".to_string(),
                        level: 1,
                        method: "level-up".to_string(),
                    },
                ],
            },
            Pokemon {
                id: 25,
                name: "pikachu".to_string(),
                display_name: "Pikachu".to_string(),
                types: vec!["electric".to_string()],
                hp: 35,
                attack: 55,
                defense: 40,
                sp_attack: 50,
                sp_defense: 50,
                speed: 90,
                height: 4,
                weight: 60,
                sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png".to_string(),
                artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png".to_string(),
                genus: "Mouse Pokémon".to_string(),
                description: "It has small electric sacs on both its cheeks. If threatened, it looses electric charges from the sacs.".to_string(),
                moves: vec![
                    PokemonMove {
                        name: "thunder-shock".to_string(),
                        level: 1,
                        method: "level-up".to_string(),
                    },
                ],
            },
            Pokemon {
                id: 151,
                name: "mew".to_string(),
                display_name: "Mew".to_string(),
                types: vec!["psychic".to_string()],
                hp: 100,
                attack: 100,
                defense: 100,
                sp_attack: 100,
                sp_defense: 100,
                speed: 100,
                height: 4,
                weight: 40,
                sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/151.png".to_string(),
                artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/151.png".to_string(),
                genus: "New Species Pokémon".to_string(),
                description: "A POKéMON of South America that was thought to have been extinct. It is very intelligent and learns any move.".to_string(),
                moves: vec![
                    PokemonMove {
                        name: "pound".to_string(),
                        level: 1,
                        method: "level-up".to_string(),
                    },
                ],
            },
        ];

        self.save_pokemons(&sample).await
    }

    #[allow(dead_code)]
    pub async fn save_pokemon(&self, p: &Pokemon) -> Result<(), String> {
        let conn = self.conn.lock().await;
        let types_str = serde_json::to_string(&p.types)
            .map_err(|e| format!("Failed to serialize types: {e}"))?;
        let moves_str = serde_json::to_string(&p.moves)
            .map_err(|e| format!("Failed to serialize moves: {e}"))?;

        conn.execute(
            "INSERT INTO pokemon (
                id, name, display_name, types, hp, attack, defense,
                sp_attack, sp_defense, speed, height, weight,
                sprite_url, artwork_url, genus, description, moves_json
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON CONFLICT (id) DO UPDATE SET
                name = excluded.name,
                display_name = excluded.display_name,
                types = excluded.types,
                hp = excluded.hp,
                attack = excluded.attack,
                defense = excluded.defense,
                sp_attack = excluded.sp_attack,
                sp_defense = excluded.sp_defense,
                speed = excluded.speed,
                height = excluded.height,
                weight = excluded.weight,
                sprite_url = excluded.sprite_url,
                artwork_url = excluded.artwork_url,
                genus = excluded.genus,
                description = excluded.description,
                moves_json = excluded.moves_json,
                cached_at = now();",
            params![
                p.id,
                p.name,
                p.display_name,
                types_str,
                p.hp,
                p.attack,
                p.defense,
                p.sp_attack,
                p.sp_defense,
                p.speed,
                p.height,
                p.weight,
                p.sprite_url,
                p.artwork_url,
                p.genus,
                p.description,
                moves_str,
            ],
        )
        .map_err(|e| format!("Failed to upsert pokemon {}: {e}", p.id))?;

        Ok(())
    }

    pub async fn get_pokemon_by_id(&self, id: i32) -> Result<Option<Pokemon>, String> {
        let conn = self.conn.lock().await;
        let mut stmt = conn
            .prepare(
                "SELECT id, name, display_name, types, hp, attack, defense,
                        sp_attack, sp_defense, speed, height, weight,
                        sprite_url, artwork_url, genus, description, moves_json
                 FROM pokemon WHERE id = ?;",
            )
            .map_err(|e| format!("Failed to prepare get query: {e}"))?;

        let mut rows = stmt
            .query(params![id])
            .map_err(|e| format!("Failed to query pokemon by id: {e}"))?;

        if let Some(row) = rows
            .next()
            .map_err(|e| format!("Failed to read pokemon row: {e}"))?
        {
            let p = parse_pokemon_row(row)?;
            Ok(Some(p))
        } else {
            Ok(None)
        }
    }

    pub async fn search_pokemon(
        &self,
        query: &str,
        limit: usize,
        offset: usize,
    ) -> Result<Vec<Pokemon>, String> {
        let conn = self.conn.lock().await;
        let q_clean = query.trim().to_lowercase();

        #[allow(clippy::cast_possible_wrap)]
        let limit_i64 = limit as i64;
        #[allow(clippy::cast_possible_wrap)]
        let offset_i64 = offset as i64;

        if q_clean.is_empty() {
            let mut stmt = conn
                .prepare(
                    "SELECT id, name, display_name, types, hp, attack, defense,
                            sp_attack, sp_defense, speed, height, weight,
                            sprite_url, artwork_url, genus, description, moves_json
                     FROM pokemon ORDER BY id ASC LIMIT ? OFFSET ?;",
                )
                .map_err(|e| format!("Failed to prepare search: {e}"))?;

            let mut rows = stmt
                .query(params![limit_i64, offset_i64])
                .map_err(|e| format!("Failed to query pokemon: {e}"))?;

            let mut results = Vec::new();
            while let Some(row) = rows
                .next()
                .map_err(|e| format!("Failed to read row: {e}"))?
            {
                results.push(parse_pokemon_row(row)?);
            }
            Ok(results)
        } else {
            let num_val = q_clean.trim_start_matches('#').parse::<i32>().unwrap_or(-1);
            let pattern = format!("%{q_clean}%");

            let mut stmt = conn
                .prepare(
                    "SELECT id, name, display_name, types, hp, attack, defense,
                            sp_attack, sp_defense, speed, height, weight,
                            sprite_url, artwork_url, genus, description, moves_json
                     FROM pokemon
                     WHERE id = ? OR lower(name) LIKE ? OR lower(display_name) LIKE ? OR lower(types) LIKE ?
                     ORDER BY id ASC LIMIT ? OFFSET ?;",
                )
                .map_err(|e| format!("Failed to prepare search: {e}"))?;

            let mut rows = stmt
                .query(params![
                    num_val, pattern, pattern, pattern, limit_i64, offset_i64
                ])
                .map_err(|e| format!("Failed to query search results: {e}"))?;

            let mut results = Vec::new();
            while let Some(row) = rows
                .next()
                .map_err(|e| format!("Failed to read row: {e}"))?
            {
                results.push(parse_pokemon_row(row)?);
            }
            Ok(results)
        }
    }

    pub async fn total_pokemon_count(&self, query: &str) -> Result<usize, String> {
        let conn = self.conn.lock().await;
        let q_clean = query.trim().to_lowercase();

        if q_clean.is_empty() {
            let count: i64 = conn
                .query_row("SELECT count(*) FROM pokemon;", [], |r| r.get(0))
                .map_err(|e| format!("Failed to get count: {e}"))?;
            Ok(usize::try_from(count).unwrap_or(0))
        } else {
            let num_val = q_clean.trim_start_matches('#').parse::<i32>().unwrap_or(-1);
            let pattern = format!("%{q_clean}%");

            let count: i64 = conn
                .query_row(
                    "SELECT count(*) FROM pokemon
                     WHERE id = ? OR lower(name) LIKE ? OR lower(display_name) LIKE ? OR lower(types) LIKE ?;",
                    params![num_val, pattern, pattern, pattern],
                    |r| r.get(0),
                )
                .map_err(|e| format!("Failed to get search count: {e}"))?;
            Ok(usize::try_from(count).unwrap_or(0))
        }
    }

    // --- Diagnostic and Debug Inspection methods ---

    pub async fn list_tables(&self) -> Result<Vec<String>, String> {
        let conn = self.conn.lock().await;
        let mut stmt = conn
            .prepare(
                "SELECT table_name FROM information_schema.tables
                 WHERE table_schema = 'main'
                 ORDER BY table_name;",
            )
            .map_err(|e| format!("Failed to list tables: {e}"))?;

        let mut rows = stmt
            .query([])
            .map_err(|e| format!("Failed to query tables: {e}"))?;

        let mut tables = Vec::new();
        while let Some(row) = rows
            .next()
            .map_err(|e| format!("Failed to read table name: {e}"))?
        {
            let name: String = row.get(0).unwrap_or_default();
            tables.push(name);
        }
        Ok(tables)
    }

    pub async fn describe_table(&self, table: &str) -> Result<Vec<ColumnInfo>, String> {
        let conn = self.conn.lock().await;
        let mut stmt = conn
            .prepare(
                "SELECT column_name, data_type, is_nullable
                 FROM information_schema.columns
                 WHERE table_schema = 'main' AND table_name = ?
                 ORDER BY ordinal_position;",
            )
            .map_err(|e| format!("Failed to describe table {table}: {e}"))?;

        let mut rows = stmt
            .query(params![table])
            .map_err(|e| format!("Failed to query columns for {table}: {e}"))?;

        let mut columns = Vec::new();
        while let Some(row) = rows
            .next()
            .map_err(|e| format!("Failed to read column info: {e}"))?
        {
            let name: String = row.get(0).unwrap_or_default();
            let data_type: String = row.get(1).unwrap_or_default();
            let nullable_str: String = row.get(2).unwrap_or_default();
            let is_nullable = nullable_str.to_uppercase() == "YES";
            let is_pk = name == "id";

            columns.push(ColumnInfo {
                name,
                data_type,
                is_nullable,
                is_pk,
            });
        }
        Ok(columns)
    }

    pub async fn table_row_count(&self, table: &str) -> Result<i64, String> {
        let conn = self.conn.lock().await;
        // Verify table name is alphanumeric/underscore to prevent injection
        if !table.chars().all(|c| c.is_alphanumeric() || c == '_') {
            return Err("Invalid table name".to_string());
        }

        let query = format!("SELECT count(*) FROM {table};");
        let count: i64 = conn
            .query_row(&query, [], |r| r.get(0))
            .map_err(|e| format!("Failed to get row count for {table}: {e}"))?;
        Ok(count)
    }

    pub async fn preview_table(&self, table: &str, limit: usize) -> Result<TablePreview, String> {
        if !table.chars().all(|c| c.is_alphanumeric() || c == '_') {
            return Err("Invalid table name".to_string());
        }
        let total_rows = self.table_row_count(table).await?;

        let conn = self.conn.lock().await;
        let query = format!("SELECT * FROM {table} LIMIT {limit};");
        let mut stmt = conn
            .prepare(&query)
            .map_err(|e| format!("Failed to prepare preview for {table}: {e}"))?;

        let mut rows = stmt
            .query([])
            .map_err(|e| format!("Failed to execute preview for {table}: {e}"))?;

        let col_count = rows.as_ref().map_or(0, duckdb::Statement::column_count);
        let mut col_names = Vec::new();
        if let Some(r) = rows.as_ref() {
            for i in 0..col_count {
                col_names.push(
                    r.column_name(i)
                        .map_or_else(|_| "?".to_string(), String::from),
                );
            }
        }

        let mut data_rows = Vec::new();
        while let Some(row) = rows
            .next()
            .map_err(|e| format!("Failed to read preview row: {e}"))?
        {
            let mut row_vals = Vec::new();
            for i in 0..col_count {
                let val: duckdb::types::Value = row.get(i).unwrap_or(duckdb::types::Value::Null);
                row_vals.push(format_duck_val(&val));
            }
            data_rows.push(row_vals);
        }

        Ok(TablePreview {
            table_name: table.to_string(),
            total_rows,
            columns: col_names,
            rows: data_rows,
        })
    }
}

fn format_duck_val(val: &duckdb::types::Value) -> String {
    match val {
        duckdb::types::Value::Null => "NULL".to_string(),
        duckdb::types::Value::Boolean(b) => b.to_string(),
        duckdb::types::Value::TinyInt(n) => n.to_string(),
        duckdb::types::Value::SmallInt(n) => n.to_string(),
        duckdb::types::Value::Int(n) => n.to_string(),
        duckdb::types::Value::BigInt(n) => n.to_string(),
        duckdb::types::Value::HugeInt(n) => n.to_string(),
        duckdb::types::Value::UTinyInt(n) => n.to_string(),
        duckdb::types::Value::USmallInt(n) => n.to_string(),
        duckdb::types::Value::UInt(n) => n.to_string(),
        duckdb::types::Value::UBigInt(n) => n.to_string(),
        duckdb::types::Value::Float(f) => format!("{f:.2}"),
        duckdb::types::Value::Double(f) => format!("{f:.2}"),
        duckdb::types::Value::Text(s) => s.clone(),
        other => format!("{other:?}"),
    }
}

fn parse_pokemon_row(row: &duckdb::Row) -> Result<Pokemon, String> {
    let id: i32 = row.get(0).map_err(|e| format!("col 0 id: {e}"))?;
    let name: String = row.get(1).map_err(|e| format!("col 1 name: {e}"))?;
    let display_name: String = row.get(2).map_err(|e| format!("col 2 display_name: {e}"))?;
    let types_json: String = row.get(3).map_err(|e| format!("col 3 types: {e}"))?;
    let hp: i32 = row.get(4).map_err(|e| format!("col 4 hp: {e}"))?;
    let attack: i32 = row.get(5).map_err(|e| format!("col 5 attack: {e}"))?;
    let defense: i32 = row.get(6).map_err(|e| format!("col 6 defense: {e}"))?;
    let sp_attack: i32 = row.get(7).map_err(|e| format!("col 7 sp_attack: {e}"))?;
    let sp_defense: i32 = row.get(8).map_err(|e| format!("col 8 sp_defense: {e}"))?;
    let speed: i32 = row.get(9).map_err(|e| format!("col 9 speed: {e}"))?;
    let height: i32 = row.get(10).map_err(|e| format!("col 10 height: {e}"))?;
    let weight: i32 = row.get(11).map_err(|e| format!("col 11 weight: {e}"))?;
    let sprite_url: String = row.get(12).map_err(|e| format!("col 12 sprite_url: {e}"))?;
    let artwork_url: String = row
        .get(13)
        .map_err(|e| format!("col 13 artwork_url: {e}"))?;
    let genus: String = row.get(14).map_err(|e| format!("col 14 genus: {e}"))?;
    let description: String = row
        .get(15)
        .map_err(|e| format!("col 15 description: {e}"))?;
    let moves_json: String = row.get(16).map_err(|e| format!("col 16 moves_json: {e}"))?;

    let types: Vec<String> = serde_json::from_str(&types_json).unwrap_or_default();
    let moves: Vec<PokemonMove> = serde_json::from_str(&moves_json).unwrap_or_default();

    Ok(Pokemon {
        id,
        name,
        display_name,
        types,
        hp,
        attack,
        defense,
        sp_attack,
        sp_defense,
        speed,
        height,
        weight,
        sprite_url,
        artwork_url,
        genus,
        description,
        moves,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_duckdb_lifecycle() {
        let db = DuckDb::new(":memory:").expect("new in-memory duckdb");
        db.init().await.expect("init duckdb");

        let count = db.seed_test_data().await.expect("seed duckdb");
        assert_eq!(count, 5, "Should seed 5 test Pokemon");

        let bulbasaur = db
            .get_pokemon_by_id(1)
            .await
            .unwrap()
            .expect("bulbasaur exists");
        assert_eq!(bulbasaur.display_name, "Bulbasaur");
        assert_eq!(bulbasaur.types, vec!["grass", "poison"]);
        assert!(bulbasaur.hp > 0);
        assert!(!bulbasaur.moves.is_empty());

        let mew = db
            .get_pokemon_by_id(151)
            .await
            .unwrap()
            .expect("mew exists");
        assert_eq!(mew.display_name, "Mew");

        // Search test
        let search_grass = db.search_pokemon("grass", 50, 0).await.unwrap();
        assert!(!search_grass.is_empty());
        assert!(search_grass.iter().any(|p| p.name == "bulbasaur"));

        let search_id = db.search_pokemon("25", 10, 0).await.unwrap();
        assert!(search_id.iter().any(|p| p.name == "pikachu"));

        // Table list & describe
        let tables = db.list_tables().await.unwrap();
        assert!(tables.contains(&"pokemon".to_string()));

        let cols = db.describe_table("pokemon").await.unwrap();
        assert!(cols.iter().any(|c| c.name == "display_name"));

        let preview = db.preview_table("pokemon", 5).await.unwrap();
        assert_eq!(preview.rows.len(), 5);
    }
}