//! DECLARATIVE migration of `tests/e2e/issue_1574_wrapped_down_scroll.rs`.
//!
//! Issue #1574: "Weird scrolling behavior in a buffer with a lot of
//! line wrapping." The original e2e file ran 7 tests across a
//! width sweep with iterative search-until-marker loops. Each
//! iteration's load-bearing precondition was discovered at runtime
//! (e.g. "press Ctrl+Up until the cursor row is empty and
//! paragraph two is hidden") — fundamentally control flow, not
//! data.
//!
//! ## What's migrated declaratively here
//!
//! * **Down-arrow walk** (`migrated_..._down_arrow_scrolling_invariants_rendered`):
//!   encoded as one `LayoutScenario` per (width, height) tuple
//!   from the original sweep, each with a fixed `MoveDown`
//!   sequence + per-step renders (required so the wrap-aware
//!   `compute_wrap_aware_visual_move_fallback` sees a fresh
//!   layout cache) and `RowMatch::AnyRowContains(END_MARKER)`
//!   for the "reached EOF" claim.
//!
//! * **Up-arrow walk** (`migrated_..._up_arrow_scrolling_invariants_rendered`):
//!   mirror with `MoveDocumentEnd` + N `MoveUp` and
//!   `RowMatch::AnyRowContains(TOP_MARKER)`.
//!
//! * **Anti-test**: zero `MoveDown` actions; the viewport must
//!   stay at top, and `END_MARKER` must not be visible. Pins
//!   that the positive test's "reaches EOF" claim depends on
//!   the action sequence.
//!
//! ## What's deferred
//!
//! The original file's remaining tests are all width-sweep +
//! iterative-conditional setup (`while cursor_row_is_empty &&
//! paragraph_hidden { ctrl_up }`). Translating those exit
//! conditions into a static action sequence requires per-(width,
//! height) byte offsets that aren't stable across editor
//! revisions:
//!
//! * `migrated_issue_1574_down_from_empty_line_at_bottom_lands_on_paragraph_start`
//! * `migrated_issue_1574_up_from_empty_line_at_top_lands_on_paragraph_end`
//! * `migrated_issue_1574_crlf_fixture_down_jump_lands_on_paragraph_start`
//! * `migrated_issue_1574_crlf_fixture_up_jump_lands_on_paragraph_end`
//! * `migrated_issue_1574_ctrl_up_down_scroll_roundtrip_sweep`
//!
//! These are marked `#[ignore]` (citing #2058). To avoid silently
//! losing live coverage, the original e2e file is RESTORED at
//! `tests/e2e/issue_1574_wrapped_down_scroll.rs` (re-added to
//! `tests/e2e/mod.rs`) so these scenarios keep running until the
//! declarative DSL can express them. Re-enabling the declarative
//! stubs needs either:
//!   1. A `LayoutScenario.actions_until: Vec<UntilPredicate>` DSL
//!      extension that drives actions in a loop until a
//!      predicate over the snapshot returns true (the original
//!      drives `loop { ctrl_up; if cursor_row_empty &&
//!      para2_hidden { break } }` with mid-loop invariant checks),
//!      or
//!   2. Per-(width, height) lookup tables of the exact MoveDown
//!      counts needed to park the cursor at the empty separator
//!      at each terminal geometry.
//!
//! This was assessed as too large a framework surface to land
//! faithfully in this session (the loops carry multi-outcome
//! convergence rules and per-iteration invariants, not just a
//! single terminating predicate), so the deferral-with-e2e-restore
//! path is taken per the no-silent-gaps rule.
//!
//! ## DSL extensions added in this migration
//!
//! * `LayoutScenario.initial_file: Option<PathBuf>` — open a
//!   fixture from disk via the harness `open_file` path instead
//!   of seeding from `initial_text`.
//! * `LayoutScenario.step_assertions: Vec<StepAssertion>` — assert
//!   `RenderSnapshotExpect` after a specific action index, with
//!   a render between every step. The walk-sweep tests use this
//!   to feed the wrap-aware MoveUp/MoveDown fallback with a
//!   fresh layout cache.
//! * `LayoutScenario.viewport_top_byte_distinct_at_most:
//!   Option<usize>` — cross-step cap on distinct `top_byte`
//!   values across `step_assertions` snapshots.
//!
//! Source: `tests/e2e/issue_1574_wrapped_down_scroll.rs`
//! (2 of 7 tests migrated declaratively; 5 deferred — e2e source
//! restored so coverage is not lost; 1 anti-test).

use crate::common::scenario::layout_scenario::{
    assert_layout_scenario, LayoutScenario, ScenarioConfigOverrides, StepAssertion,
};
use crate::common::scenario::render_snapshot::{RenderSnapshotExpect, RowMatch};
use fresh::test_api::Action;
use std::path::PathBuf;

fn wrap_overrides() -> ScenarioConfigOverrides {
    ScenarioConfigOverrides {
        line_wrap: Some(true),
        ..Default::default()
    }
}

fn fixture_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("issue_1574_wrapped_lines.md")
}

/// Distinctive marker on the final line of the wrapped-lines fixture.
const END_MARKER: &str = "End of the wrapped-buffer scroll fixture.";

/// Distinctive marker on the first line of the wrapped-lines fixture.
const TOP_MARKER: &str = "# Wrapped Buffer Scroll Test";

/// Build per-step snapshot expectations for every action in
/// `count_inclusive` (0..count). Forces a render between each
/// action, which the wrap-aware MoveDown/MoveUp fallback needs to
/// see a fresh layout cache.
fn snapshot_every_step(count: usize, start: usize) -> Vec<StepAssertion> {
    (start..count)
        .map(|i| StepAssertion {
            after_action_index: i,
            expect: RenderSnapshotExpect::default(),
        })
        .collect()
}

// =====================================================================
// Down-arrow walk: cursor walks Down through the fixture, the
// end-of-file marker must eventually appear on screen.
// =====================================================================

#[test]
fn migrated_issue_1574_down_arrow_scrolling_invariants_rendered() {
    // Original: `test_issue_1574_down_arrow_scrolling_invariants_rendered`.
    // Width sweep [60, 80, 100] × heights [20, 28] — sparser
    // than the original e2e's [60, 70, 80, 90, 100] to keep
    // wall-time bounded; the regression invariant is geometry-
    // independent so any sample of the sweep captures it.
    let widths: [u16; 3] = [60, 80, 100];
    let heights: [u16; 2] = [20, 28];
    // 150 MoveDowns is enough to walk the 29-line fixture at any
    // of the sweep widths (worst case ≈ 100+ visual rows at width
    // 60). Each step renders so the wrap-aware MoveDown path
    // (`compute_wrap_aware_visual_move_fallback`) sees a fresh
    // layout cache — without that, MoveDown falls back to the
    // byte-based logical-line variant that advances only one
    // visual row at a time.
    let actions: Vec<Action> = std::iter::repeat(Action::MoveDown).take(150).collect();
    let step_assertions = snapshot_every_step(actions.len(), 0);
    for &height in &heights {
        for &width in &widths {
            assert_layout_scenario(LayoutScenario {
                description: format!(
                    "Down-arrow walk reaches EOF marker (width={width}, height={height})"
                ),
                initial_file: Some(fixture_path()),
                width,
                height,
                actions: actions.clone(),
                config_overrides: wrap_overrides(),
                step_assertions: step_assertions.clone(),
                expected_snapshot: RenderSnapshotExpect {
                    row_checks: vec![RowMatch::AnyRowContains(END_MARKER.into())],
                    viewport_top_byte_greater_than: Some(0),
                    ..Default::default()
                },
                ..Default::default()
            });
        }
    }
}

#[test]
fn migrated_issue_1574_up_arrow_scrolling_invariants_rendered() {
    // Original: `test_issue_1574_up_arrow_scrolling_invariants_rendered`.
    // Mirror of the Down sweep — walks from EOF back to BOF.
    let widths: [u16; 3] = [60, 80, 100];
    let heights: [u16; 2] = [20, 28];
    let mut actions = vec![Action::MoveDocumentEnd];
    actions.extend(std::iter::repeat(Action::MoveUp).take(150));
    // Render between each MoveUp so the wrap-aware MoveUp fallback
    // sees a fresh layout cache. Skip step 0 (MoveDocumentEnd
    // doesn't need wrap-aware support to land at EOF).
    let step_assertions = snapshot_every_step(actions.len(), 1);
    for &height in &heights {
        for &width in &widths {
            assert_layout_scenario(LayoutScenario {
                description: format!(
                    "Up-arrow walk reaches top marker (width={width}, height={height})"
                ),
                initial_file: Some(fixture_path()),
                width,
                height,
                actions: actions.clone(),
                config_overrides: wrap_overrides(),
                step_assertions: step_assertions.clone(),
                expected_snapshot: RenderSnapshotExpect {
                    row_checks: vec![RowMatch::AnyRowContains(TOP_MARKER.into())],
                    ..Default::default()
                },
                ..Default::default()
            });
        }
    }
}

// =====================================================================
// Deferred tests — see the file docstring for rationale.
// =====================================================================

/// DEFERRED: original used iterative `Ctrl+Up until empty row +
/// paragraph hidden` to park the cursor on the empty separator at
/// the bottom of the viewport. The declarative DSL needs an
/// `actions_until: Vec<UntilPredicate>` extension (or per-(width,
/// height) lookup tables of MoveDown counts) before this can be
/// migrated faithfully. Source: `tests/e2e/issue_1574_wrapped_down_scroll.rs`.
#[test]
#[ignore = "deferred: needs iterative-conditional actions_until DSL extension; e2e source restored at tests/e2e/issue_1574_wrapped_down_scroll.rs; #2058"]
fn migrated_issue_1574_down_from_empty_line_at_bottom_lands_on_paragraph_start() {}

/// DEFERRED: mirror of the down-jump test above; same reason.
#[test]
#[ignore = "deferred: needs iterative-conditional actions_until DSL extension; e2e source restored at tests/e2e/issue_1574_wrapped_down_scroll.rs; #2058"]
fn migrated_issue_1574_up_from_empty_line_at_top_lands_on_paragraph_end() {}

/// DEFERRED: same shape as the down-jump variant but on a
/// CRLF-encoded fixture. Re-enabling needs the same DSL extension
/// the non-CRLF jump test does (the fixture-loading path via
/// `LayoutScenario.initial_file` is already wired and tested by
/// the walk-sweep tests above).
#[test]
#[ignore = "deferred: needs iterative-conditional actions_until DSL extension; e2e source restored at tests/e2e/issue_1574_wrapped_down_scroll.rs; #2058"]
fn migrated_issue_1574_crlf_fixture_down_jump_lands_on_paragraph_start() {}

/// DEFERRED: Up-direction mirror of the CRLF down-jump.
#[test]
#[ignore = "deferred: needs iterative-conditional actions_until DSL extension; e2e source restored at tests/e2e/issue_1574_wrapped_down_scroll.rs; #2058"]
fn migrated_issue_1574_crlf_fixture_up_jump_lands_on_paragraph_end() {}

/// DEFERRED: original walked Ctrl+Up / Ctrl+Down / Down at each
/// step and asserted the top content row returned to its prior
/// value after each Ctrl+Up/Ctrl+Down pair. The per-step "Ctrl+Up
/// always scrolls when not at top of buffer" branch requires
/// runtime branching on `is_viewport_at_top`. Re-enabling needs
/// either per-step expectations expressed as predicates over the
/// snapshot (not currently in `RenderSnapshotExpect`) or
/// `actions_until`.
#[test]
#[ignore = "deferred: needs predicate-step or actions_until DSL extension; e2e source restored at tests/e2e/issue_1574_wrapped_down_scroll.rs; #2058"]
fn migrated_issue_1574_ctrl_up_down_scroll_roundtrip_sweep() {}

// =====================================================================
// Anti-test.
// =====================================================================

/// Anti-test: drop every `MoveDown` press from the positive
/// Down-arrow sweep flow. Without the arrow walk, the viewport
/// never scrolls — so `END_MARKER` must NOT appear on screen and
/// `TOP_MARKER` (the first line of the fixture) must remain
/// visible. Pins that the positive test's "eventually reaches EOF"
/// invariant is load-bearing on the `MoveDown` action sequence,
/// not on the fixture/config setup alone.
#[test]
fn anti_issue_1574_wrapped_dropping_down_keeps_top_row_pinned() {
    assert_layout_scenario(LayoutScenario {
        description: "anti: no Down ⇒ viewport stays at top, END_MARKER not visible".into(),
        initial_file: Some(fixture_path()),
        width: 80,
        height: 20,
        actions: vec![],
        config_overrides: wrap_overrides(),
        expected_top_byte: Some(0),
        expected_snapshot: RenderSnapshotExpect {
            row_checks: vec![
                RowMatch::AnyRowContains(TOP_MARKER.into()),
                RowMatch::NoRowContains(END_MARKER.into()),
            ],
            ..Default::default()
        },
        ..Default::default()
    });
}
