//! Integration tests for the `show <kind>` list commands (V5):
//! `show tables`, `show relationships`, `show indexes`.
//!
//! Covers:
//! - Parse layer: each form parses to `Command::ShowList { kind }`
//!   in both simple and advanced mode (the forms are
//!   `CommandCategory::Simple`, available in both).
//! - Worker round-trip: `Database::show_list` returns the correct
//!   pre-formatted lines after real DDL (tables, a relationship,
//!   an index), and the empty-collection wording.
//! - App end-to-end: submitting `show tables` reaches the output
//!   panel as system lines and marks the echo complete.

use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};

use rdbms_playground::action::Action;
use rdbms_playground::app::App;
use rdbms_playground::db::Database;
use rdbms_playground::dsl::{
    ColumnSpec, Command, ReferentialAction, ShowListKind, Type, parse_command,
};
use rdbms_playground::event::AppEvent;
use rdbms_playground::mode::Mode;
use rdbms_playground::persistence::Persistence;
use rdbms_playground::project;

// =================================================================
// Parse layer
// =================================================================

#[test]
fn show_tables_parses_as_show_list_tables() {
    assert_eq!(
        parse_command("show tables").expect("parses"),
        Command::ShowList {
            kind: ShowListKind::Tables,
            name: None,
        },
    );
}

#[test]
fn show_relationships_parses_as_show_list_relationships() {
    assert_eq!(
        parse_command("show relationships").expect("parses"),
        Command::ShowList {
            kind: ShowListKind::Relationships,
            name: None,
        },
    );
}

#[test]
fn show_indexes_parses_as_show_list_indexes() {
    assert_eq!(
        parse_command("show indexes").expect("parses"),
        Command::ShowList {
            kind: ShowListKind::Indexes,
            name: None,
        },
    );
}

#[test]
fn show_relationship_singular_parses_with_name() {
    assert_eq!(
        parse_command("show relationship orders_customer").expect("parses"),
        Command::ShowList {
            kind: ShowListKind::Relationships,
            name: Some("orders_customer".to_string()),
        },
    );
}

#[test]
fn show_index_singular_parses_with_name() {
    assert_eq!(
        parse_command("show index idx_orders_customer").expect("parses"),
        Command::ShowList {
            kind: ShowListKind::Indexes,
            name: Some("idx_orders_customer".to_string()),
        },
    );
}

#[test]
fn show_table_singular_still_parses_as_show_table() {
    // The new plural keyword must not shadow the singular
    // `show table <name>` form — `table` ≠ `tables`.
    assert_eq!(
        parse_command("show table Customers").expect("parses"),
        Command::ShowTable {
            name: "Customers".to_string()
        },
    );
}

// =================================================================
// Worker round-trip — real execution
// =================================================================

fn rt() -> tokio::runtime::Runtime {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("tokio rt")
}

fn open_project_db() -> (project::Project, Database, tempfile::TempDir) {
    let dir = tempfile::tempdir().expect("create tempdir");
    let project = project::open_or_create(None, Some(dir.path())).expect("open or create project");
    let persistence = Persistence::new(project.path().to_path_buf());
    let db = Database::open_with_persistence(project.db_path(), persistence)
        .expect("open db with persistence");
    (project, db, dir)
}

/// Create two related tables plus an index, so each list kind has
/// content. Returns once the worker has applied everything.
async fn seed_schema(db: &Database) {
    db.create_table(
        "Customers".to_string(),
        vec![
            ColumnSpec::new("id", Type::Serial),
            ColumnSpec::new("Name", Type::Text),
        ],
        vec!["id".to_string()],
        None,
    )
    .await
    .expect("create Customers");
    db.create_table(
        "Orders".to_string(),
        vec![
            ColumnSpec::new("id", Type::Serial),
            ColumnSpec::new("customer_id", Type::Int),
        ],
        vec!["id".to_string()],
        None,
    )
    .await
    .expect("create Orders");
    db.add_relationship(
        Some("orders_customer".to_string()),
        "Customers".to_string(),
        vec!["id".to_string()],
        "Orders".to_string(),
        vec!["customer_id".to_string()],
        ReferentialAction::Cascade,
        ReferentialAction::NoAction,
        false,
        None,
    )
    .await
    .expect("add relationship");
    db.add_index(
        Some("idx_orders_customer".to_string()),
        "Orders".to_string(),
        vec!["customer_id".to_string()],
        None,
    )
    .await
    .expect("add index");
}

#[test]
fn show_tables_lists_all_user_tables_with_count_header() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    let lines = rt
        .block_on(db.show_list(ShowListKind::Tables, None))
        .expect("show tables");
    assert_eq!(lines[0], "Tables (2):", "count header");
    assert!(
        lines.iter().any(|l| l == "  Customers"),
        "Customers listed: {lines:?}",
    );
    assert!(
        lines.iter().any(|l| l == "  Orders"),
        "Orders listed: {lines:?}",
    );
}

#[test]
fn show_relationships_lists_name_endpoints_and_nondefault_action() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    let lines = rt
        .block_on(db.show_list(ShowListKind::Relationships, None))
        .expect("show relationships");
    assert_eq!(lines[0], "Relationships (1):");
    // Name, both endpoints, and the non-default ON DELETE CASCADE
    // (ON UPDATE NO ACTION is the default and is omitted).
    assert_eq!(
        lines[1], "  orders_customer: Customers.id → Orders.customer_id on delete cascade",
        "relationship summary line: {lines:?}",
    );
}

#[test]
fn show_indexes_lists_qualified_name_and_columns() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    let lines = rt
        .block_on(db.show_list(ShowListKind::Indexes, None))
        .expect("show indexes");
    assert_eq!(lines[0], "Indexes (1):");
    assert_eq!(
        lines[1], "  Orders.idx_orders_customer (customer_id)",
        "index summary line: {lines:?}",
    );
}

#[test]
fn show_lists_report_empty_collections_with_friendly_lines() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    // No schema seeded — every kind is empty.
    assert_eq!(
        rt.block_on(db.show_list(ShowListKind::Tables, None))
            .unwrap(),
        vec!["No tables in this project yet.".to_string()],
    );
    assert_eq!(
        rt.block_on(db.show_list(ShowListKind::Relationships, None))
            .unwrap(),
        vec!["No relationships in this project yet.".to_string()],
    );
    assert_eq!(
        rt.block_on(db.show_list(ShowListKind::Indexes, None))
            .unwrap(),
        vec!["No indexes in this project yet.".to_string()],
    );
}

// =================================================================
// V5a — singular per-item detail
// =================================================================

#[test]
fn show_one_relationship_renders_detail_block() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    let lines = rt
        .block_on(db.show_list(
            ShowListKind::Relationships,
            Some("orders_customer".to_string()),
        ))
        .expect("show relationship");
    assert_eq!(lines[0], "Relationship `orders_customer`:");
    assert_eq!(lines[1], "  Customers.id → Orders.customer_id");
    assert!(
        lines.iter().any(|l| l == "  on delete cascade"),
        "on-delete shown: {lines:?}",
    );
}

#[test]
fn show_one_index_renders_detail_block() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    let lines = rt
        .block_on(db.show_list(
            ShowListKind::Indexes,
            Some("idx_orders_customer".to_string()),
        ))
        .expect("show index");
    assert_eq!(lines[0], "Index `idx_orders_customer` on Orders:");
    assert!(
        lines.iter().any(|l| l == "  columns: customer_id"),
        "columns shown: {lines:?}",
    );
    assert!(
        lines.iter().any(|l| l == "  unique: no"),
        "uniqueness shown: {lines:?}",
    );
}

#[test]
fn show_one_unknown_name_reports_friendly_not_found() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    assert_eq!(
        rt.block_on(db.show_list(ShowListKind::Relationships, Some("nope".to_string())))
            .unwrap(),
        vec!["No relationship named `nope`.".to_string()],
    );
    assert_eq!(
        rt.block_on(db.show_list(ShowListKind::Indexes, Some("nope".to_string())))
            .unwrap(),
        vec!["No index named `nope`.".to_string()],
    );
}

// =================================================================
// App end-to-end
// =================================================================

const fn key(code: KeyCode) -> AppEvent {
    AppEvent::Key(KeyEvent {
        code,
        modifiers: KeyModifiers::NONE,
        kind: KeyEventKind::Press,
        state: crossterm::event::KeyEventState::NONE,
    })
}

fn type_str(app: &mut App, s: &str) {
    for c in s.chars() {
        app.update(key(KeyCode::Char(c)));
    }
}

#[test]
fn app_show_tables_dispatches_show_list_command() {
    let mut app = App::new();
    app.mode = Mode::Simple;
    type_str(&mut app, "show tables");
    let actions = app.update(key(KeyCode::Enter));
    let dispatched = actions.iter().any(|a| {
        matches!(
            a,
            Action::ExecuteDsl {
                command: Command::ShowList {
                    kind: ShowListKind::Tables,
                    name: None,
                },
                ..
            }
        )
    });
    assert!(
        dispatched,
        "submit dispatches ShowList(Tables): {actions:?}"
    );
}

#[test]
fn app_renders_show_list_lines_as_system_output() {
    // Feed the success event directly so the test stays
    // self-contained (the worker round-trip is covered above).
    let mut app = App::new();
    app.output
        .push_back(rdbms_playground::app::OutputLine::echo(
            "show tables",
            Mode::Simple,
        ));
    app.update(AppEvent::DslShowListSucceeded {
        command: Command::ShowList {
            kind: ShowListKind::Tables,
            name: None,
        },
        lines: vec!["Tables (1):".to_string(), "  Customers".to_string()],
    });
    assert!(
        app.output.iter().any(|l| l.text == "Tables (1):"),
        "header line rendered",
    );
    assert!(
        app.output.iter().any(|l| l.text == "  Customers"),
        "item line rendered",
    );
}

// =================================================================
// ADR-0044 — `show relationship <name>` renders a diagram
// =================================================================

#[test]
fn show_relationship_worker_returns_structured_diagram_data() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    let data = rt
        .block_on(db.show_relationship("orders_customer".to_string()))
        .expect("show_relationship ok")
        .expect("relationship found");
    assert_eq!(data.rel.name, "orders_customer");
    // child = FK holder, parent = referenced (ADR-0044 left/right).
    assert_eq!(data.child.name, "Orders");
    assert_eq!(data.parent.name, "Customers");
    assert_eq!(data.rel.child_columns, vec!["customer_id".to_string()]);
    assert_eq!(data.rel.parent_columns, vec!["id".to_string()]);
}

#[test]
fn show_relationship_worker_returns_none_for_unknown_name() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    assert!(
        rt.block_on(db.show_relationship("nope".to_string()))
            .expect("ok")
            .is_none(),
        "unknown relationship → None",
    );
}

#[test]
fn app_renders_show_relationship_as_a_styled_diagram() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    let data = rt
        .block_on(db.show_relationship("orders_customer".to_string()))
        .expect("ok")
        .expect("found");

    let mut app = App::new();
    app.output
        .push_back(rdbms_playground::app::OutputLine::echo(
            "show relationship orders_customer",
            Mode::Simple,
        ));
    app.update(AppEvent::DslShowRelationshipSucceeded {
        command: Command::ShowList {
            kind: ShowListKind::Relationships,
            name: Some("orders_customer".to_string()),
        },
        data: Some(data),
    });
    let text: String = app
        .output
        .iter()
        .map(|l| l.text.as_str())
        .collect::<Vec<_>>()
        .join("\n");
    // Both tables, box-drawing, the connector arrow, the actions line.
    assert!(text.contains("Orders"), "child box: {text}");
    assert!(text.contains("Customers"), "parent box: {text}");
    assert!(
        text.contains('┌') && text.contains('│'),
        "box drawing: {text}"
    );
    assert!(text.contains('▶'), "connector arrow: {text}");
    assert!(text.contains("on delete cascade"), "actions: {text}");
    // The diagram lines are styled (per-span runs), not plain system.
    assert!(
        app.output.iter().any(|l| l.styled_runs.is_some()),
        "diagram lines carry styled runs",
    );
}

#[test]
fn app_show_relationship_not_found_shows_friendly_line() {
    let mut app = App::new();
    app.output
        .push_back(rdbms_playground::app::OutputLine::echo(
            "show relationship nope",
            Mode::Simple,
        ));
    app.update(AppEvent::DslShowRelationshipSucceeded {
        command: Command::ShowList {
            kind: ShowListKind::Relationships,
            name: Some("nope".to_string()),
        },
        data: None,
    });
    assert!(
        app.output
            .iter()
            .any(|l| l.text == "No relationship named `nope`."),
        "friendly not-found line",
    );
}

#[test]
fn app_show_table_renders_relationships_as_compact_diagrams() {
    let (_p, db, _dir) = open_project_db();
    let rt = rt();
    rt.block_on(seed_schema(&db));
    // Orders holds the FK to Customers — an outbound relationship.
    let desc = rt
        .block_on(db.describe_table("Orders".to_string()))
        .expect("describe Orders");

    let mut app = App::new();
    app.output
        .push_back(rdbms_playground::app::OutputLine::echo(
            "show table Orders",
            Mode::Simple,
        ));
    app.update(AppEvent::DslSucceeded {
        command: Command::ShowTable {
            name: "Orders".to_string(),
        },
        description: Some(desc),
        echo: None,
    });
    let text: String = app
        .output
        .iter()
        .map(|l| l.text.as_str())
        .collect::<Vec<_>>()
        .join("\n");
    // The focal structure box, then a diagram (not the prose block).
    assert!(text.contains("Relationships"), "diagram heading: {text}");
    assert!(!text.contains("References:"), "prose suppressed: {text}");
    assert!(text.contains("Customers"), "neighbour box: {text}");
    assert!(text.contains('▶'), "connector arrow: {text}");
}
