mod common;

use axum::http::StatusCode;
use tower::util::ServiceExt;

use doctate_common::oneliners::OnelinerState;

use common::{
    DOCUMENT_FILE, TestConfig, body_string, get_with_cookie, paths, seed_case, seed_oneliner_ready,
    seed_oneliner_state, seed_recording, test_admin, test_user,
};

/// Password "s" is the shared test default; wrap `login` so call sites
/// don't repeat the literal.
async fn login_dr_a(app: &axum::Router) -> String {
    common::login(app, "dr_a", "s").await
}

// ---------------------------------------------------------------------
// GET /web/cases/{id}  — case page
// ---------------------------------------------------------------------

#[tokio::test]
async fn case_page_shows_document_when_present() {
    let cfg = TestConfig::new()
        .with_label("cp-doc")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    std::fs::write(case_dir.join(DOCUMENT_FILE), "der Inhalt").unwrap();

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_string(resp).await;

    assert!(body.contains("der Inhalt"), "document body missing");
    assert!(
        body.contains(&paths::case_recordings(case_id)),
        "recordings sub-page link missing"
    );
}

#[tokio::test]
async fn case_page_document_has_copy_button() {
    let cfg = TestConfig::new()
        .with_label("cp-copy")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    std::fs::write(case_dir.join(DOCUMENT_FILE), "kopiere mich").unwrap();

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(body.contains(r#"id="copy-btn""#), "copy button missing");
    assert!(
        body.contains(r#"id="doc-content""#),
        "doc-content wrapper missing (JS depends on it)"
    );
}

#[tokio::test]
async fn case_page_shows_analyze_button_when_transcribed_without_document() {
    let (cfg, settings) = TestConfig::new()
        .with_label("cp-analyze")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build_pair();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_recording(
        &case_dir,
        "2026-04-15T10-00-00Z",
        Some("transkribierter Text"),
    );

    let app = doctate_server::create_router_with_settings(cfg, settings);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(
        body.contains(&format!(r#"action="{}""#, paths::case_analyze(case_id))),
        "analyze form missing"
    );
    assert!(
        !body.contains(r#"id="doc-content""#),
        "doc-content must not appear without a document"
    );
}

/// Bug regression: a previous LLM run that failed (e.g. transient HTTP
/// transport error to the inference endpoint) leaves a `.analysis_failed.json`
/// marker on disk. With no `document.md` yet, the existing "Neu analysieren"
/// button — which lives inside the document-rendered branch — is invisible,
/// and the auto-trigger refuses to retry as long as the marker matches the
/// current input. The case page must therefore surface a recovery banner
/// that exposes the reason and a retry form, otherwise the user is stuck.
///
/// Doctor variant: only the default-backend "Erneut versuchen" button is
/// shown; per-backend choice is admin-only (covered by the sibling test).
#[tokio::test]
async fn case_page_shows_failure_banner_with_retry_when_marker_present() {
    let (cfg, settings) = TestConfig::new()
        .with_label("cp-fail-banner")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build_pair();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_recording(
        &case_dir,
        "2026-04-15T10-00-00Z",
        Some("transkribierter Text"),
    );

    // Seed the marker exactly as the analyze worker would on a transport
    // failure. `last_recording_mtime` is intentionally close to (but not
    // exactly equal to) the freshly-seeded recording's mtime — the banner
    // path reads the marker unconditionally, so equality is irrelevant
    // here; we only need the file to exist and parse.
    std::fs::write(
        case_dir.join(".analysis_failed.json"),
        r#"{"last_recording_mtime":"2026-04-15T10:00:00Z","reason":"llm: connect timeout to https://example.invalid/v1/chat/completions","failed_at":"2026-04-15T10:05:00Z"}"#,
    )
    .unwrap();

    let app = doctate_server::create_router_with_settings(cfg, settings);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_string(resp).await;

    assert!(
        body.contains(r#"class="failure-banner""#),
        "failure-banner div missing"
    );
    assert!(
        body.contains("Analyse fehlgeschlagen"),
        "user-facing failure title missing"
    );
    assert!(
        body.contains("connect timeout"),
        "raw reason text not surfaced (expected inside <details>)"
    );
    assert!(
        body.contains(r#"datetime="2026-04-15T10:05:00Z""#),
        "failed_at not rendered as <time datetime=...> for browser locale formatting"
    );
    assert!(
        body.contains(&format!(r#"action="{}""#, paths::case_analyze(case_id))),
        "retry form must POST to the analyze endpoint"
    );
    assert!(
        body.contains(">Erneut versuchen</button>"),
        "default retry button missing (expected 'Erneut versuchen')"
    );
    assert!(
        !body.contains("Erneut mit "),
        "per-backend retry buttons must not be shown to non-admins"
    );
    assert!(
        !body.contains("Wird analysiert"),
        "the analyzing placeholder must not coexist with the failure banner"
    );
}

/// Admin variant of the failure-banner test: in addition to the default
/// "Erneut versuchen" button shown to every user, an admin sees one
/// "Erneut mit <backend>" button per configured backend, letting them
/// re-analyse with a different model when one is misbehaving. Pins the
/// `{% if is_admin %}` gate in `case_page.html` so a future refactor
/// can't silently expose backend choice to all users (matches the
/// `analyze` handler's 403 guard) or remove it from admins.
#[tokio::test]
async fn case_page_failure_banner_admin_can_choose_backend() {
    let (cfg, settings) = TestConfig::new()
        .with_label("cp-fail-banner-admin")
        .with_user(test_admin("dr_a"))
        .with_llm("http://unused")
        .build_pair();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_recording(
        &case_dir,
        "2026-04-15T10-00-00Z",
        Some("transkribierter Text"),
    );
    std::fs::write(
        case_dir.join(".analysis_failed.json"),
        r#"{"last_recording_mtime":"2026-04-15T10:00:00Z","reason":"llm: connect timeout","failed_at":"2026-04-15T10:05:00Z"}"#,
    )
    .unwrap();

    let app = doctate_server::create_router_with_settings(cfg, settings);
    let cookie = common::login(&app, "dr_a", "s").await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_string(resp).await;

    assert!(
        body.contains(r#"class="failure-banner""#),
        "failure-banner div missing for admin"
    );
    assert!(
        body.contains(">Erneut versuchen</button>"),
        "default retry button must remain visible for admin too"
    );
    assert!(
        body.contains("Erneut mit "),
        "admin must see at least one 'Erneut mit <backend>' button"
    );
}

#[tokio::test]
#[ignore = "post-refactor any_backend_available() reads IONOS_API_KEY which TestConfig pre-sets; cannot be unset for one test in a shared process"]
async fn case_page_shows_llm_missing_hint_without_llm() {
    let cfg = TestConfig::new()
        .with_label("cp-llm")
        .with_user(test_user("dr_a"))
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_recording(&case_dir, "2026-04-15T10-00-00Z", Some("Text"));

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(
        body.contains("LLM-Analyse nicht konfiguriert"),
        "expected llm-missing hint"
    );
}

#[tokio::test]
async fn case_page_empty_case_shows_placeholder() {
    let cfg = TestConfig::new()
        .with_label("cp-empty")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    seed_case(&cfg.data_path, "dr_a", case_id);

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_string(resp).await;

    assert!(
        body.contains("Noch keine Aufnahmen vorhanden"),
        "empty-case placeholder missing"
    );
    assert!(
        body.contains(&format!(r#"action="{}""#, paths::case_close(case_id))),
        "close form missing on empty case"
    );
}

#[tokio::test]
async fn case_page_foreign_user_returns_404() {
    let cfg = TestConfig::new()
        .with_label("cp-idor")
        .with_user(test_user("dr_a"))
        .with_user(test_user("dr_b"))
        .with_llm("http://unused")
        .build();
    let case_id = "22222222-2222-2222-2222-222222222222";
    // Case exists under dr_b, but session is dr_a → 404.
    let case_dir = seed_case(&cfg.data_path, "dr_b", case_id);
    std::fs::write(case_dir.join(DOCUMENT_FILE), "fremdes Dokument").unwrap();

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

// ---------------------------------------------------------------------
// GET /web/cases/{id}/recordings  — recordings sub-page
// ---------------------------------------------------------------------

#[tokio::test]
async fn case_recordings_shows_all_recordings() {
    let cfg = TestConfig::new()
        .with_label("rec-list")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_recording(&case_dir, "2026-04-15T10-00-00Z", Some("erste Aufnahme"));
    seed_recording(&case_dir, "2026-04-15T11-00-00Z", None);

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_recordings(case_id), &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_string(resp).await;

    assert!(body.contains("2026-04-15T10-00-00Z.m4a"));
    assert!(body.contains("2026-04-15T11-00-00Z.m4a"));
    assert!(body.contains("erste Aufnahme"));
    assert!(body.contains("Transkription ausstehend"));
}

#[tokio::test]
async fn case_recordings_back_link_targets_case_page() {
    let cfg = TestConfig::new()
        .with_label("rec-back")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_recording(&case_dir, "2026-04-15T10-00-00Z", None);

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_recordings(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(
        body.contains(&format!(r#"href="{}""#, paths::case_detail(case_id))),
        "back link must target the case page, not /web/cases"
    );
}

#[tokio::test]
async fn case_page_uses_oneliner_as_h1_when_present() {
    let cfg = TestConfig::new()
        .with_label("cp-h1")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_oneliner_ready(&case_dir, "Herzkatheter unauffällig");
    std::fs::write(case_dir.join(DOCUMENT_FILE), "Inhalt").unwrap();

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    // H1 wraps the oneliner; the short case_id fallback must be gone.
    assert!(
        body.contains("<h1>") && body.contains("Herzkatheter unauffällig"),
        "h1 should show the oneliner"
    );
    // The prominent meta block with the full UUID is admin-only.
    assert!(
        !body.contains(r#"<div class="meta">"#),
        "non-admin must not see the full case_id meta block"
    );
}

/// A case with only silent recordings (empty 0-byte transcript) and a
/// persisted `OnelinerState::Empty` must render the "unbenannt" variant
/// of the oneliner, never "generiere Titel …". Regression guard for
/// the bug where the UI's `compute_oneliner_display` collapsed
/// `TranscriptState::Silent` onto "transcribed" and then chose
/// `Generating` because no state was persisted — the combination left
/// cases stuck on "generiere Titel …" forever.
#[tokio::test]
async fn case_page_silent_only_shows_empty_not_generating() {
    let cfg = TestConfig::new()
        .with_label("cp-silent")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "33333333-3333-3333-3333-333333333333";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    // Some("") → 0-byte transcript sidecar — whisper-as-silence output.
    seed_recording(&case_dir, "2026-04-15T10-00-00Z", Some(""));
    seed_oneliner_state(
        &case_dir,
        &OnelinerState::Empty {
            generated_at: "2026-04-21T11:00:00Z".into(),
        },
    );

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(
        !body.contains("generiere Titel"),
        "silent-only case must not be stuck on 'generiere Titel …'"
    );
    assert!(
        body.contains("unbenannt"),
        "expected oneliner to render as 'unbenannt' for Empty state"
    );
}

#[tokio::test]
async fn case_page_falls_back_to_generic_title_for_empty_state() {
    // An `Empty` state (LLM deliberately returned no medical content) is
    // not a text; the H1 must fall back to the generic "Fall" label, not
    // leak the kind string or crash the template.
    let cfg = TestConfig::new()
        .with_label("cp-empty-gen")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "22222222-2222-2222-2222-222222222222";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_oneliner_state(
        &case_dir,
        &OnelinerState::Empty {
            generated_at: "2026-04-18T10:00:00Z".into(),
        },
    );

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(body.contains("Fall"), "expected generic 'Fall' fallback");
    assert!(
        !body.contains("\"kind\""),
        "must not leak JSON kind discriminator into HTML"
    );
}

#[tokio::test]
async fn case_page_admin_sees_admin_view_toggle() {
    let cfg = TestConfig::new()
        .with_label("cp-toggle")
        .with_user(test_admin("dr_admin"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    seed_case(&cfg.data_path, "dr_admin", case_id);

    let app = doctate_server::create_router(cfg);
    let cookie = common::login(&app, "dr_admin", "s").await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(
        body.contains(r#"id="admin-view-toggle""#),
        "admin must see the admin-view toggle checkbox"
    );
}

#[tokio::test]
async fn case_page_non_admin_has_no_admin_view_toggle() {
    let cfg = TestConfig::new()
        .with_label("cp-no-toggle")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    seed_case(&cfg.data_path, "dr_a", case_id);

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(
        !body.contains(r#"id="admin-view-toggle""#),
        "non-admin must not see the admin-view toggle"
    );
}

#[tokio::test]
async fn case_page_admin_sees_full_case_id_meta() {
    let cfg = TestConfig::new()
        .with_label("cp-admin")
        .with_user(test_admin("dr_admin"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    seed_case(&cfg.data_path, "dr_admin", case_id);

    let app = doctate_server::create_router(cfg);
    let cookie = common::login(&app, "dr_admin", "s").await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(
        body.contains(case_id),
        "admin meta block should expose the full case_id"
    );
}

#[tokio::test]
async fn case_recordings_breadcrumb_includes_oneliner() {
    let cfg = TestConfig::new()
        .with_label("rec-breadcrumb")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_oneliner_ready(&case_dir, "Kurzer Befund");
    seed_recording(&case_dir, "2026-04-15T10-00-00Z", None);

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_recordings(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    // Two separate breadcrumb links: back to the case list and back to the case page.
    assert!(
        body.contains(&format!(r#"href="{}""#, paths::CASES)),
        "breadcrumb must link back to the case list"
    );
    assert!(
        body.contains(&format!(r#"href="{}""#, paths::case_detail(case_id))),
        "breadcrumb must link back to the case page"
    );
    assert!(
        body.contains("Kurzer Befund"),
        "breadcrumb must show the oneliner text"
    );
}

#[tokio::test]
async fn case_recordings_has_no_action_buttons() {
    let cfg = TestConfig::new()
        .with_label("rec-ro")
        .with_user(test_user("dr_a"))
        .with_llm("http://unused")
        .build();
    let case_id = "11111111-1111-1111-1111-111111111111";
    let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
    seed_recording(&case_dir, "2026-04-15T10-00-00Z", Some("Text"));

    let app = doctate_server::create_router(cfg);
    let cookie = login_dr_a(&app).await;

    let resp = app
        .oneshot(get_with_cookie(paths::case_recordings(case_id), &cookie))
        .await
        .unwrap();
    let body = body_string(resp).await;

    assert!(
        !body.contains(&format!(r#"action="{}""#, paths::case_analyze(case_id))),
        "recordings page must not expose the analyze action"
    );
    assert!(
        !body.contains(&format!(r#"action="{}""#, paths::case_close(case_id))),
        "recordings page must not expose the close action"
    );
}
