use super::*;
use api_types::{DiaryEntryDto, MovieDto, ReviewDto};
use uuid::Uuid;

fn setup_app() -> App {
    App {
        screen: Screen::Setup(SetupState {
            api_url: String::new(),
            error: None,
        }),
        token: None,
        loading: false,
        status: None,
        api_url: String::new(),
    }
}

fn login_app() -> App {
    App {
        screen: Screen::Login(LoginState::default()),
        token: None,
        loading: false,
        status: None,
        api_url: String::new(),
    }
}

fn main_app() -> App {
    App {
        screen: Screen::Main(MainState::new("http://localhost:3000".into())),
        token: Some("tok".into()),
        loading: false,
        status: None,
        api_url: "http://localhost:3000".into(),
    }
}

fn diary_entry() -> DiaryEntryDto {
    DiaryEntryDto {
        movie: MovieDto {
            id: Uuid::new_v4(),
            title: "The Matrix".into(),
            release_year: 1999,
            director: None,
            poster_path: None,
            genres: vec![],
            runtime_minutes: None,
            original_language: None,
            overview: None,
            collection_name: None,
        },
        review: ReviewDto {
            id: Uuid::new_v4(),
            rating: 5,
            comment: None,
            watched_at: "1999-03-31T00:00:00".into(),
            watch_medium: None,
        },
    }
}

// ── Setup screen ──────────────────────────────────────────────────────────

#[test]
fn setup_input_char_appends_to_api_url() {
    let mut app = setup_app();
    update(&mut app, Action::InputChar('h'));
    update(&mut app, Action::InputChar('i'));
    if let Screen::Setup(s) = &app.screen {
        assert_eq!(s.api_url, "hi");
    } else {
        panic!("expected Setup");
    }
}

#[test]
fn setup_submit_with_empty_url_sets_error() {
    let mut app = setup_app();
    let cmds = update(&mut app, Action::SetupSubmit);
    assert!(cmds.is_empty());
    if let Screen::Setup(s) = &app.screen {
        assert!(s.error.is_some());
    } else {
        panic!("expected Setup");
    }
}

#[test]
fn setup_submit_with_url_saves_config_and_transitions_to_login() {
    let mut app = setup_app();
    update(&mut app, Action::InputChar('h'));
    let cmds = update(&mut app, Action::SetupSubmit);
    assert!(cmds.iter().any(|c| matches!(c, Command::SaveConfig(_))));
    assert!(matches!(app.screen, Screen::Login(_)));
}

// ── Login screen ──────────────────────────────────────────────────────────

#[test]
fn login_input_char_goes_to_email_by_default() {
    let mut app = login_app();
    update(&mut app, Action::InputChar('a'));
    if let Screen::Login(s) = &app.screen {
        assert_eq!(s.email, "a");
        assert_eq!(s.password, "");
    } else {
        panic!();
    }
}

#[test]
fn login_focus_next_moves_to_password() {
    let mut app = login_app();
    update(&mut app, Action::FocusNext);
    if let Screen::Login(s) = &app.screen {
        assert_eq!(s.focused, LoginField::Password);
    } else {
        panic!();
    }
}

#[test]
fn login_input_after_focus_goes_to_password() {
    let mut app = login_app();
    update(&mut app, Action::FocusNext);
    update(&mut app, Action::InputChar('x'));
    if let Screen::Login(s) = &app.screen {
        assert_eq!(s.password, "x");
    } else {
        panic!();
    }
}

#[test]
fn login_submit_returns_login_command_and_sets_loading() {
    let mut app = login_app();
    for c in "user@example.com".chars() {
        update(&mut app, Action::InputChar(c));
    }
    update(&mut app, Action::FocusNext);
    for c in "pass123".chars() {
        update(&mut app, Action::InputChar(c));
    }
    let cmds = update(&mut app, Action::LoginSubmit);
    assert!(cmds.iter().any(|c| matches!(c, Command::Login { .. })));
    assert!(app.loading);
}

#[test]
fn login_submit_with_empty_fields_sets_error_status() {
    let mut app = login_app();
    let cmds = update(&mut app, Action::LoginSubmit);
    assert!(cmds.is_empty());
    assert!(app.status.as_ref().is_some_and(|s| s.is_error));
}

#[test]
fn auth_ok_sets_token_and_transitions_to_main() {
    let mut app = login_app();
    let cmds = update(&mut app, Action::AuthOk("jwt-token".into()));
    assert_eq!(app.token, Some("jwt-token".into()));
    assert!(matches!(app.screen, Screen::Main(_)));
    assert!(!app.loading);
    assert!(cmds.iter().any(|c| matches!(c, Command::SaveToken(_))));
    assert!(cmds.iter().any(|c| matches!(c, Command::LoadDiary { .. })));
}

#[test]
fn auth_fail_sets_error_status_and_clears_loading() {
    let mut app = login_app();
    app.loading = true;
    update(&mut app, Action::AuthFail("bad creds".into()));
    assert!(!app.loading);
    assert!(app.status.as_ref().is_some_and(|s| s.is_error));
}

// ── Diary ─────────────────────────────────────────────────────────────────

#[test]
fn diary_scroll_down_increments_selected() {
    let mut app = main_app();
    update(
        &mut app,
        Action::DiaryLoaded {
            entries: vec![diary_entry(), diary_entry(), diary_entry()],
            total: 3,
        },
    );
    update(&mut app, Action::ScrollDown);
    if let Screen::Main(m) = &app.screen {
        assert_eq!(m.diary.selected, 1);
    } else {
        panic!();
    }
}

#[test]
fn diary_scroll_up_clamps_at_zero() {
    let mut app = main_app();
    update(
        &mut app,
        Action::DiaryLoaded {
            entries: vec![diary_entry()],
            total: 1,
        },
    );
    update(&mut app, Action::ScrollUp);
    if let Screen::Main(m) = &app.screen {
        assert_eq!(m.diary.selected, 0);
    } else {
        panic!();
    }
}

#[test]
fn diary_scroll_down_clamps_at_last_entry() {
    let mut app = main_app();
    update(
        &mut app,
        Action::DiaryLoaded {
            entries: vec![diary_entry()],
            total: 1,
        },
    );
    update(&mut app, Action::ScrollDown);
    if let Screen::Main(m) = &app.screen {
        assert_eq!(m.diary.selected, 0);
    } else {
        panic!();
    }
}

#[test]
fn delete_init_sets_delete_pending() {
    let mut app = main_app();
    let entry = diary_entry();
    let review_id = entry.review.id;
    update(
        &mut app,
        Action::DiaryLoaded {
            entries: vec![entry],
            total: 1,
        },
    );
    update(&mut app, Action::DeleteInit);
    if let Screen::Main(m) = &app.screen {
        assert_eq!(m.diary.delete_pending, Some(review_id));
    } else {
        panic!();
    }
}

#[test]
fn delete_confirm_returns_delete_command() {
    let mut app = main_app();
    let entry = diary_entry();
    let review_id = entry.review.id;
    update(
        &mut app,
        Action::DiaryLoaded {
            entries: vec![entry],
            total: 1,
        },
    );
    update(&mut app, Action::DeleteInit);
    let cmds = update(&mut app, Action::DeleteConfirm);
    assert!(
        cmds.iter()
            .any(|c| matches!(c, Command::DeleteReview(id) if *id == review_id))
    );
}

#[test]
fn delete_cancel_clears_pending() {
    let mut app = main_app();
    let entry = diary_entry();
    update(
        &mut app,
        Action::DiaryLoaded {
            entries: vec![entry],
            total: 1,
        },
    );
    update(&mut app, Action::DeleteInit);
    update(&mut app, Action::DeleteCancel);
    if let Screen::Main(m) = &app.screen {
        assert!(m.diary.delete_pending.is_none());
    } else {
        panic!();
    }
}

#[test]
fn review_deleted_removes_entry_from_list() {
    let mut app = main_app();
    let entry = diary_entry();
    let review_id = entry.review.id;
    update(
        &mut app,
        Action::DiaryLoaded {
            entries: vec![entry],
            total: 1,
        },
    );
    update(&mut app, Action::ReviewDeleted(review_id));
    if let Screen::Main(m) = &app.screen {
        assert!(m.diary.entries.is_empty());
        assert_eq!(m.diary.total, 0);
    } else {
        panic!();
    }
}

// ── Add Review ────────────────────────────────────────────────────────────

#[test]
fn rating_up_increments_rating() {
    let mut app = main_app();
    if let Screen::Main(m) = &mut app.screen {
        m.tab = Tab::AddReview;
        m.add_review.rating = 3;
    }
    update(&mut app, Action::RatingUp);
    if let Screen::Main(m) = &app.screen {
        assert_eq!(m.add_review.rating, 4);
    }
}

#[test]
fn rating_clamps_at_5() {
    let mut app = main_app();
    if let Screen::Main(m) = &mut app.screen {
        m.tab = Tab::AddReview;
        m.add_review.rating = 5;
    }
    update(&mut app, Action::RatingUp);
    if let Screen::Main(m) = &app.screen {
        assert_eq!(m.add_review.rating, 5);
    }
}

#[test]
fn review_submit_returns_create_review_command() {
    let mut app = main_app();
    if let Screen::Main(m) = &mut app.screen {
        m.tab = Tab::AddReview;
        m.add_review.title = "The Matrix".into();
        m.add_review.watched_at = "1999-03-31T00:00:00".into();
        m.add_review.rating = 5;
    }
    let cmds = update(&mut app, Action::ReviewSubmit);
    assert!(cmds.iter().any(|c| matches!(c, Command::CreateReview(_))));
}

#[test]
fn review_submit_with_missing_title_and_id_sets_error() {
    let mut app = main_app();
    if let Screen::Main(m) = &mut app.screen {
        m.tab = Tab::AddReview;
        m.add_review.watched_at = "1999-03-31T00:00:00".into();
    }
    let cmds = update(&mut app, Action::ReviewSubmit);
    assert!(cmds.is_empty());
    assert!(app.status.as_ref().is_some_and(|s| s.is_error));
}

// ── Bulk Import ───────────────────────────────────────────────────────────

#[test]
fn bulk_import_all_with_valid_rows_returns_import_next_command() {
    let mut app = main_app();
    if let Screen::Main(m) = &mut app.screen {
        m.tab = Tab::BulkImport;
        m.bulk_import.stage = BulkImportStage::Preview;
        m.bulk_import.parsed = vec![ParsedRow {
            row: 2,
            result: Ok(LogReviewRequest {
                external_metadata_id: None,
                manual_title: Some("The Matrix".into()),
                manual_release_year: None,
                manual_director: None,
                rating: 5,
                comment: None,
                watched_at: "1999-03-31T00:00:00".into(),
                watch_medium: None,
            }),
        }];
    }
    let cmds = update(&mut app, Action::BulkImportAll);
    assert!(cmds.iter().any(|c| matches!(c, Command::ImportNext(0))));
}

#[test]
fn bulk_item_done_advances_stage_and_returns_next_command() {
    let mut app = main_app();
    if let Screen::Main(m) = &mut app.screen {
        m.tab = Tab::BulkImport;
        m.bulk_import.stage = BulkImportStage::Importing { done: 0 };
        m.bulk_import.valid_requests = vec![
            LogReviewRequest {
                external_metadata_id: None,
                manual_title: Some("A".into()),
                manual_release_year: None,
                manual_director: None,
                rating: 5,
                comment: None,
                watched_at: "2024-01-01T00:00:00".into(),
                watch_medium: None,
            },
            LogReviewRequest {
                external_metadata_id: None,
                manual_title: Some("B".into()),
                manual_release_year: None,
                manual_director: None,
                rating: 4,
                comment: None,
                watched_at: "2024-01-02T00:00:00".into(),
                watch_medium: None,
            },
        ];
        m.bulk_import.results = vec![None, None];
    }
    let cmds = update(
        &mut app,
        Action::BulkItemDone {
            index: 0,
            error: None,
        },
    );
    assert!(cmds.iter().any(|c| matches!(c, Command::ImportNext(1))));
}

#[test]
fn bulk_item_done_last_item_transitions_to_done() {
    let mut app = main_app();
    if let Screen::Main(m) = &mut app.screen {
        m.tab = Tab::BulkImport;
        m.bulk_import.stage = BulkImportStage::Importing { done: 0 };
        m.bulk_import.valid_requests = vec![LogReviewRequest {
            external_metadata_id: None,
            manual_title: Some("A".into()),
            manual_release_year: None,
            manual_director: None,
            rating: 5,
            comment: None,
            watched_at: "2024-01-01T00:00:00".into(),
            watch_medium: None,
        }];
        m.bulk_import.results = vec![None];
    }
    let cmds = update(
        &mut app,
        Action::BulkItemDone {
            index: 0,
            error: None,
        },
    );
    assert!(cmds.is_empty());
    if let Screen::Main(m) = &app.screen {
        assert!(matches!(m.bulk_import.stage, BulkImportStage::Done));
    }
    assert!(app.status.is_some());
}

// ── Settings ──────────────────────────────────────────────────────────────

#[test]
fn settings_save_returns_save_config_command() {
    let mut app = main_app();
    if let Screen::Main(m) = &mut app.screen {
        m.tab = Tab::Settings;
        m.settings.api_url = "http://new-server:8080".into();
    }
    let cmds = update(&mut app, Action::SettingsSave);
    assert!(
        cmds.iter()
            .any(|c| matches!(c, Command::SaveConfig(url) if url.contains("8080")))
    );
}

#[test]
fn settings_logout_clears_token_and_goes_to_login() {
    let mut app = main_app();
    let cmds = update(&mut app, Action::SettingsLogout);
    assert!(app.token.is_none());
    assert!(matches!(app.screen, Screen::Login(_)));
    assert!(cmds.iter().any(|c| matches!(c, Command::ClearToken)));
}

#[test]
fn auth_ok_uses_app_api_url_for_main_state() {
    let mut app = login_app();
    app.api_url = "http://test-server:9000".into();
    update(&mut app, Action::AuthOk("tok".into()));
    if let Screen::Main(m) = &app.screen {
        assert_eq!(m.settings.api_url, "http://test-server:9000");
    } else {
        panic!("expected Main");
    }
}

// ── parse_csv ─────────────────────────────────────────────────────────────

// CSV column order matches the export format:
// title,year,director,rating,comment,watched_at,external_metadata_id

#[test]
fn parse_csv_valid_row_with_title() {
    let csv = "title,year,director,rating,comment,watched_at,external_metadata_id\nThe Matrix,1999,Wachowski,5,,1999-03-31T00:00:00,\n";
    let rows = parse_csv(csv);
    assert_eq!(rows.len(), 1);
    assert!(rows[0].result.is_ok());
    let req = rows[0].result.as_ref().unwrap();
    assert_eq!(req.manual_title.as_deref(), Some("The Matrix"));
    assert_eq!(req.manual_director.as_deref(), Some("Wachowski"));
    assert_eq!(req.rating, 5);
}

#[test]
fn parse_csv_row_missing_title_and_id_is_error() {
    let csv = "title,year,director,rating,comment,watched_at,external_metadata_id\n,,,5,,2024-01-01T00:00:00,\n";
    let rows = parse_csv(csv);
    assert_eq!(rows.len(), 1);
    assert!(rows[0].result.is_err());
}

#[test]
fn parse_csv_invalid_rating_is_error() {
    let csv = "title,year,director,rating,comment,watched_at,external_metadata_id\nThe Matrix,,,9,,2024-01-01T00:00:00,\n";
    let rows = parse_csv(csv);
    assert!(rows[0].result.is_err());
}

#[test]
fn parse_csv_with_external_id_only() {
    let csv = "title,year,director,rating,comment,watched_at,external_metadata_id\n,,,5,,1999-03-31T00:00:00,tt0133093\n";
    let rows = parse_csv(csv);
    assert!(rows[0].result.is_ok());
    let req = rows[0].result.as_ref().unwrap();
    assert_eq!(req.external_metadata_id.as_deref(), Some("tt0133093"));
    assert!(req.manual_title.is_none());
}

#[test]
fn parse_csv_rating_zero_is_valid() {
    let csv = "title,year,director,rating,comment,watched_at,external_metadata_id\nThe Matrix,,,0,,2024-01-01T00:00:00,\n";
    let rows = parse_csv(csv);
    assert_eq!(rows.len(), 1);
    assert!(rows[0].result.is_ok());
    let req = rows[0].result.as_ref().unwrap();
    assert_eq!(req.rating, 0);
}
