//! Textbildschirm-Emulation: Zellenpuffer (Unicode-Zeichen + Farbattribut),
//! Cursor, Scrollen.
//!
//! Der Puffer liegt in der Laufzeitbibliothek und ist frei von jeder
//! Terminal-Abhängigkeit: `PRINT`, `LOCATE`, `CLS`, `COLOR` und `SCREEN()`
//! sind Bibliothekselemente und müssen ihn erreichen, und jedes
//! Bildschirmverhalten bleibt ohne Terminal prüfbar. Die Darstellung liegt in
//! `tb-ui` (`ScreenWidget`).
//!
//! Entscheidungen (siehe PLAN.md):
//! - durchgängig Unicode, keine CP437-Emulation
//! - **dynamische Größe**: der Bildschirm folgt der Terminalgröße; unterhalb
//!   der Mindestgröße (80×25) wird nur ein Hinweis gerendert (btop-artig),
//!   größere Terminals werden voll genutzt.
//!
//! Zeichen belegen so viele Zellen, wie ihre Darstellungsbreite verlangt
//! (Entscheidung 2026-09-02): schmale Zeichen eine, breite (Emoji, CJK)
//! zwei. Die zweite Zelle ist als Fortsetzung markiert und wird nie
//! eigenständig beschrieben.

use unicode_width::UnicodeWidthChar;

/// Mindestgröße; darunter wird nur ein Hinweis angezeigt.
pub const MIN_COLS: usize = 80;
pub const MIN_ROWS: usize = 25;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScreenError;

/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
    pub ch: char,
    /// Vordergrund 0–15 (klassische Palette).
    pub fg: u8,
    /// Hintergrund 0–7.
    pub bg: u8,
    /// Zweite Hälfte eines breiten Zeichens. Solche Zellen tragen kein
    /// eigenes Zeichen und werden bei der Darstellung übersprungen.
    pub fortsetzung: bool,
}

impl Default for Cell {
    fn default() -> Self {
        Cell {
            ch: ' ',
            fg: 7,
            bg: 0,
            fortsetzung: false,
        }
    }
}

/// Der emulierte Textbildschirm mit dynamischer Größe.
///
/// Koordinaten in der öffentlichen API sind 1-basiert (Zeile, Spalte),
/// wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts.
#[derive(Debug, Clone)]
pub struct TextScreen {
    cols: usize,
    rows: usize,
    cells: Vec<Cell>,
    /// Cursorposition, 0-basiert intern.
    cur_row: usize,
    cur_col: usize,
    pub cursor_visible: bool,
    /// Aktuelle Ausgabefarben (`COLOR`).
    pub fg: u8,
    pub bg: u8,
    /// Scrollbereich (`VIEW PRINT`), 0-basiert inklusiv.
    view_top: usize,
    view_bottom: usize,
    /// true, solange kein eigenes `VIEW PRINT` gesetzt ist — der
    /// Scrollbereich folgt dann der Bildschirmgröße.
    view_full: bool,
    /// Je Zeile die größte beschriebene Spalte (0-basiert), `None` für
    /// unberührte Zeilen. Grundlage des getrimmten Snapshots: nachgestellte
    /// Leerzeichen, die `PRINT` selbst ausgibt, zählen als belegt, ein nie
    /// beschriebener Zeilenrest nicht.
    belegt: Vec<Option<usize>>,
    /// Seit der letzten Anzeige verändert? Der Host wird nur dann zum
    /// Neuzeichnen aufgefordert.
    veraendert: bool,
    graphic_x: i32,
    graphic_y: i32,
    graphic_view: Option<(i32, i32, i32, i32)>,
}

impl Default for TextScreen {
    fn default() -> Self {
        Self::new()
    }
}

impl TextScreen {
    /// Bildschirm in Mindestgröße (80×25).
    pub fn new() -> Self {
        Self::with_size(MIN_COLS, MIN_ROWS)
    }

    /// Bildschirm in gegebener Größe (wird auf die Mindestgröße angehoben).
    pub fn with_size(cols: usize, rows: usize) -> Self {
        let cols = cols.max(MIN_COLS);
        let rows = rows.max(MIN_ROWS);
        TextScreen {
            cols,
            rows,
            cells: vec![Cell::default(); cols * rows],
            cur_row: 0,
            cur_col: 0,
            cursor_visible: true,
            fg: 7,
            bg: 0,
            view_top: 0,
            view_bottom: rows - 1,
            view_full: true,
            belegt: vec![None; rows],
            veraendert: true,
            graphic_x: 0,
            graphic_y: 0,
            graphic_view: None,
        }
    }

    pub fn cols(&self) -> usize {
        self.cols
    }
    pub fn rows(&self) -> usize {
        self.rows
    }

    /// An neue Terminalgröße anpassen: Inhalt bleibt oben links erhalten,
    /// neue Zellen sind leer. Unterhalb der Mindestgröße bleibt der Puffer
    /// bei 80×25 (das Widget zeigt dann den Zu-klein-Hinweis).
    pub fn resize(&mut self, cols: usize, rows: usize) {
        self.veraendert = true;
        let cols = cols.max(MIN_COLS);
        let rows = rows.max(MIN_ROWS);
        if cols == self.cols && rows == self.rows {
            return;
        }
        let mut cells = vec![
            Cell {
                ch: ' ',
                fg: self.fg,
                bg: self.bg,
                fortsetzung: false
            };
            cols * rows
        ];
        for row in 0..self.rows.min(rows) {
            for col in 0..self.cols.min(cols) {
                cells[row * cols + col] = self.cells[row * self.cols + col];
            }
        }
        self.cells = cells;
        let mut belegt = vec![None; rows];
        for (r, value) in belegt.iter_mut().enumerate().take(self.rows.min(rows)) {
            *value = self.belegt[r].map(|c| c.min(cols - 1));
        }
        self.belegt = belegt;
        self.cols = cols;
        self.rows = rows;
        self.cur_row = self.cur_row.min(rows - 1);
        self.cur_col = self.cur_col.min(cols - 1);
        if self.view_full || self.view_top > rows - 1 {
            // Ohne eigenes VIEW PRINT folgt der Bereich dem Bildschirm; ein
            // Bereich, der ganz vom Schirm gefallen ist, wäre leer und fällt
            // auf den vollen Bildschirm zurück.
            self.view_top = 0;
            self.view_bottom = rows - 1;
            self.view_full = true;
        } else {
            self.view_bottom = self.view_bottom.min(rows - 1);
        }
    }

    /// `CLS`: Scrollbereich mit aktueller Hintergrundfarbe löschen,
    /// Cursor an den Anfang des Bereichs.
    pub fn cls(&mut self) {
        self.veraendert = true;
        let blank = Cell {
            ch: ' ',
            fg: self.fg,
            bg: self.bg,
            fortsetzung: false,
        };
        for row in self.view_top..=self.view_bottom {
            self.cells[row * self.cols..(row + 1) * self.cols].fill(blank);
        }
        self.cur_row = self.view_top;
        self.cur_col = 0;
        for r in self.view_top..=self.view_bottom {
            self.belegt[r] = None;
        }
    }

    /// Hat sich seit der letzten Quittung etwas geändert?
    pub fn ist_veraendert(&self) -> bool {
        self.veraendert
    }

    /// Änderung quittieren (nach dem Anzeigen).
    pub fn veraenderung_quittieren(&mut self) {
        self.veraendert = false;
    }

    /// Anzahl belegter Zeilen (1-basiert bis zur letzten beschriebenen),
    /// 0 wenn nichts ausgegeben wurde.
    pub fn belegte_zeilen(&self) -> usize {
        self.belegt
            .iter()
            .rposition(|b| b.is_some())
            .map_or(0, |i| i + 1)
    }

    /// Breite der belegten Zeile `z` (1-basiert); 0 für unberührte Zeilen.
    pub fn belegte_breite(&self, z: usize) -> usize {
        self.belegt
            .get(z - 1)
            .copied()
            .flatten()
            .map_or(0, |c| c + 1)
    }

    /// `COLOR vg, hg` (vg 0–31, hg 0–7). Blinkende Vordergrundfarben
    /// (16–31) werden als „hell" simuliert (Entscheidung 2026-09-02):
    /// echtes Terminal-Blinken ist nicht überall verfügbar.
    pub fn set_color(&mut self, fg: u8, bg: u8) {
        self.veraendert = true;
        let fg = if fg >= 16 { (fg & 0x0F) | 8 } else { fg };
        self.fg = fg & 0x0F;
        self.bg = bg & 0x07;
    }

    /// `LOCATE zeile, spalte` (1-basiert); außerhalb → Err (Fehler 5).
    pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ScreenError> {
        if row < 1 || row > self.rows || col < 1 || col > self.cols {
            return Err(ScreenError);
        }
        self.cur_row = row - 1;
        // Auf eine Fortsetzungszelle zu zeigen bedeutet den Zeichenanfang.
        let mut c = col - 1;
        if c > 0 && self.cells[self.cur_row * self.cols + c].fortsetzung {
            c -= 1;
        }
        self.cur_col = c;
        self.veraendert = true;
        Ok(())
    }

    /// `CSRLIN` (1-basiert).
    pub fn csrlin(&self) -> usize {
        self.cur_row + 1
    }

    /// `POS(0)` (1-basiert).
    pub fn pos(&self) -> usize {
        self.cur_col + 1
    }

    /// `VIEW PRINT oben TO unten` (1-basiert).
    pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ScreenError> {
        if top < 1 || bottom > self.rows || top > bottom {
            return Err(ScreenError);
        }
        self.view_top = top - 1;
        self.view_bottom = bottom - 1;
        self.view_full = top == 1 && bottom == self.rows;
        Ok(())
    }

    pub fn cell(&self, row: usize, col: usize) -> Cell {
        self.cells[(row - 1) * self.cols + (col - 1)]
    }

    pub fn graphics_position(&self) -> (i32, i32) {
        (self.graphic_x, self.graphic_y)
    }

    pub fn graphics_view(&mut self, view: Option<(i32, i32, i32, i32)>) -> Result<(), ScreenError> {
        if view.is_some_and(|(x1, y1, x2, y2)| x1 < 0 || y1 < 0 || x1 > x2 || y1 > y2) {
            return Err(ScreenError);
        }
        self.graphic_view = view;
        Ok(())
    }

    fn graphics_cell(&mut self, x: i32, y: i32, color: i32) {
        if x < 0
            || y < 0
            || self
                .graphic_view
                .is_some_and(|(x1, y1, x2, y2)| x < x1 || x > x2 || y < y1 || y > y2)
        {
            return;
        }
        let (col, row) = (x as usize / 8, y as usize / 8);
        if col >= self.cols || row >= self.rows {
            return;
        }
        self.cells[row * self.cols + col] = Cell {
            ch: if color == 0 { ' ' } else { '█' },
            fg: color.clamp(0, 15) as u8,
            bg: self.bg,
            fortsetzung: false,
        };
        self.belegt[row] = Some(self.belegt[row].map_or(col, |last| last.max(col)));
        self.veraendert = true;
    }

    pub fn graphics_line(&mut self, from: (i32, i32), to: (i32, i32), color: i32, fill: u8) {
        let (x1, y1) = from;
        let (x2, y2) = to;
        if fill == 2 {
            for y in (y1.min(y2)..=y1.max(y2)).step_by(8) {
                for x in (x1.min(x2)..=x1.max(x2)).step_by(8) {
                    self.graphics_cell(x, y, color);
                }
            }
        } else if fill == 1 {
            self.graphics_segment(x1, y1, x2, y1, color);
            self.graphics_segment(x2, y1, x2, y2, color);
            self.graphics_segment(x2, y2, x1, y2, color);
            self.graphics_segment(x1, y2, x1, y1, color);
        } else {
            self.graphics_segment(x1, y1, x2, y2, color);
        }
        self.graphic_x = x2;
        self.graphic_y = y2;
    }

    fn graphics_segment(&mut self, mut x: i32, mut y: i32, x2: i32, y2: i32, color: i32) {
        let dx = (x2 - x).abs();
        let sx = if x < x2 { 1 } else { -1 };
        let dy = -(y2 - y).abs();
        let sy = if y < y2 { 1 } else { -1 };
        let mut error = dx + dy;
        loop {
            self.graphics_cell(x, y, color);
            if x == x2 && y == y2 {
                break;
            }
            let twice = error * 2;
            if twice >= dy {
                error += dy;
                x += sx;
            }
            if twice <= dx {
                error += dx;
                y += sy;
            }
        }
    }

    pub fn graphics_paint(&mut self, x: i32, y: i32, color: i32) {
        let (start_col, start_row) = (x.max(0) as usize / 8, y.max(0) as usize / 8);
        if start_col >= self.cols || start_row >= self.rows {
            return;
        }
        let target = self.cells[start_row * self.cols + start_col].ch;
        let replacement = if color == 0 { ' ' } else { '█' };
        if target == replacement {
            return;
        }
        let mut pending = vec![(start_col, start_row)];
        while let Some((col, row)) = pending.pop() {
            let px = col as i32 * 8;
            let py = row as i32 * 8;
            if col >= self.cols
                || row >= self.rows
                || self.cells[row * self.cols + col].ch != target
                || self
                    .graphic_view
                    .is_some_and(|(x1, y1, x2, y2)| px < x1 || px > x2 || py < y1 || py > y2)
            {
                continue;
            }
            self.graphics_cell(px, py, color);
            if col > 0 {
                pending.push((col - 1, row));
            }
            if row > 0 {
                pending.push((col, row - 1));
            }
            pending.push((col + 1, row));
            pending.push((col, row + 1));
        }
        self.graphic_x = x;
        self.graphic_y = y;
    }

    /// Text an der Cursorposition ausgeben: Umbruch am rechten Rand,
    /// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um,
    /// `\r` setzt an den Zeilenanfang.
    pub fn print(&mut self, text: &str) {
        self.veraendert = true;
        for ch in text.chars() {
            match ch {
                '\n' => self.newline(),
                '\r' => self.cur_col = 0,
                _ => self.zeichen_setzen(ch, true),
            }
        }
    }

    /// Eine UI-Zeile ohne Umbruch oder Scrollen bis zum Bildschirmrand zeichnen.
    /// Steuerzeichen werden als Leerzeichen dargestellt; breite Zeichen passen
    /// nur vollständig. Der Cursor bleibt spätestens in der letzten Spalte.
    pub fn print_line(&mut self, text: &str) {
        self.veraendert = true;
        for ch in text.chars() {
            let ch = if ch.is_control() { ' ' } else { ch };
            if self.cur_col + ch.width().unwrap_or(1).max(1) > self.cols {
                break;
            }
            self.zeichen_setzen(ch, false);
        }
        self.cur_col = self.cur_col.min(self.cols - 1);
    }

    /// Ein Zeichen an der Cursorposition setzen und den Cursor um seine
    /// Darstellungsbreite vorrücken. Ein breites Zeichen, das nicht mehr
    /// vollständig in die Zeile passt, wandert als Ganzes in die nächste;
    /// die letzte Spalte bleibt dann leer.
    fn zeichen_setzen(&mut self, ch: char, wrap: bool) {
        // ponytail: Breite 0 (kombinierende Zeichen) wird wie 1 behandelt —
        // sie bekommen eine eigene Zelle statt sich an die vorige zu hängen.
        // Ceiling: echte Kombination erst, wenn ein Korpusprogramm sie braucht.
        let breite = ch.width().unwrap_or(1).max(1);
        if self.cur_col + breite > self.cols {
            self.newline();
        }
        let attr = (self.fg, self.bg);
        let start = self.cur_row * self.cols + self.cur_col;
        self.haelften_freimachen(self.cur_col, breite);
        self.cells[start] = Cell {
            ch,
            fg: attr.0,
            bg: attr.1,
            fortsetzung: false,
        };
        for i in 1..breite {
            self.cells[start + i] = Cell {
                ch: ' ',
                fg: attr.0,
                bg: attr.1,
                fortsetzung: true,
            };
        }
        let letzte = self.cur_col + breite - 1;
        let b = &mut self.belegt[self.cur_row];
        *b = Some(b.map_or(letzte, |c| c.max(letzte)));
        self.cur_col += breite;
        if wrap && self.cur_col >= self.cols {
            self.newline();
        }
    }

    /// Vor dem Schreiben angeschnittene breite Zeichen auflösen: wird auf
    /// eine Fortsetzung geschrieben, verliert die Hälfte davor ihr Zeichen;
    /// überschreibt der neue Text den Anfang eines breiten Zeichens, wird
    /// dessen Fortsetzung geleert. Sonst blieben Geisterzeichen stehen.
    fn haelften_freimachen(&mut self, col: usize, breite: usize) {
        let zeilenanfang = self.cur_row * self.cols;
        let leer = Cell {
            ch: ' ',
            fg: self.fg,
            bg: self.bg,
            fortsetzung: false,
        };
        if col > 0 && self.cells[zeilenanfang + col].fortsetzung {
            self.cells[zeilenanfang + col - 1] = leer;
        }
        let hinter = col + breite;
        if hinter < self.cols && self.cells[zeilenanfang + hinter].fortsetzung {
            self.cells[zeilenanfang + hinter] = leer;
        }
    }

    /// Zeilenumbruch inkl. Scrollen im Scrollbereich.
    fn newline(&mut self) {
        self.cur_col = 0;
        if self.cur_row >= self.view_bottom {
            self.scroll_up();
            self.cur_row = self.view_bottom;
        } else {
            self.cur_row += 1;
        }
    }

    /// Rückschritt: Cursor eine Spalte zurück und die Zelle leeren.
    /// Am Zeilenanfang passiert nichts (Vorbild).
    pub fn rueckschritt(&mut self) {
        if self.cur_col == 0 {
            return;
        }
        self.cur_col -= 1;
        // War das gelöschte eine Fortsetzung, gehört die Hälfte davor dazu.
        if self.cells[self.cur_row * self.cols + self.cur_col].fortsetzung && self.cur_col > 0 {
            self.cells[self.cur_row * self.cols + self.cur_col] = Cell {
                ch: ' ',
                fg: self.fg,
                bg: self.bg,
                fortsetzung: false,
            };
            self.cur_col -= 1;
        }
        self.cells[self.cur_row * self.cols + self.cur_col] = Cell {
            ch: ' ',
            fg: self.fg,
            bg: self.bg,
            fortsetzung: false,
        };
        // Die gelöschte Zelle gilt nicht mehr als belegt.
        let b = &mut self.belegt[self.cur_row];
        *b = match (self.cur_col, *b) {
            (0, _) => None,
            (c, Some(alt)) => Some(alt.min(c - 1)),
            (c, None) => Some(c - 1),
        };
        self.veraendert = true;
    }

    /// Scrollbereich um eine Zeile nach oben schieben; unterste Zeile leeren.
    pub fn scroll_up(&mut self) {
        self.veraendert = true;
        let blank = Cell {
            ch: ' ',
            fg: self.fg,
            bg: self.bg,
            fortsetzung: false,
        };
        for row in self.view_top..self.view_bottom {
            let (a, b) = self.cells.split_at_mut((row + 1) * self.cols);
            a[row * self.cols..].copy_from_slice(&b[..self.cols]);
        }
        self.cells[self.view_bottom * self.cols..(self.view_bottom + 1) * self.cols].fill(blank);
        for row in self.view_top..self.view_bottom {
            self.belegt[row] = self.belegt[row + 1];
        }
        self.belegt[self.view_bottom] = None;
    }
}

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

    #[test]
    fn print_schreibt_und_bewegt_cursor() {
        let mut s = TextScreen::new();
        s.print("AB");
        assert_eq!(s.cell(1, 1).ch, 'A');
        assert_eq!(s.cell(1, 2).ch, 'B');
        assert_eq!((s.csrlin(), s.pos()), (1, 3));
    }

    #[test]
    fn ui_zeile_bleibt_am_rand_ohne_umbruch_oder_scrollen() {
        let mut s = TextScreen::new();
        s.print("oben");
        s.view_print(3, 10).unwrap();
        s.set_color(2, 4);
        for row in [10, 25] {
            s.locate(row, 78).unwrap();
            s.print_line("中xREST");
            assert_eq!(s.cell(row, 78).ch, '中');
            assert!(s.cell(row, 79).fortsetzung);
            assert_eq!(s.cell(row, 80).ch, 'x');
            assert_eq!((s.csrlin(), s.pos()), (row, 80));
            assert_eq!((s.cell(row, 80).fg, s.cell(row, 80).bg), (2, 4));
        }
        assert_eq!(s.cell(1, 1).ch, 'o');
        assert_eq!(s.cell(9, 78).ch, ' ');
        assert_eq!(s.cell(24, 78).ch, ' ');
        s.locate(2, 1).unwrap();
        s.print_line("a\nb\rc");
        assert_eq!(
            (1..=5).map(|col| s.cell(2, col).ch).collect::<String>(),
            "a b c"
        );
        s.locate(2, 80).unwrap();
        s.print_line("中");
        assert_eq!(s.cell(2, 80).ch, ' ');
        assert_eq!(s.cell(3, 1).ch, ' ');
        assert_eq!((s.csrlin(), s.pos()), (2, 80));
    }

    #[test]
    fn unicode_zeichen_belegen_eine_zelle() {
        let mut s = TextScreen::new();
        s.print("Ä☃");
        assert_eq!(s.cell(1, 1).ch, 'Ä');
        assert_eq!(s.cell(1, 2).ch, '☃');
    }

    #[test]
    fn breites_zeichen_belegt_zwei_zellen() {
        let mut s = TextScreen::new();
        s.print("中a");
        assert_eq!(s.cell(1, 1).ch, '中');
        assert!(s.cell(1, 2).fortsetzung);
        assert_eq!(s.cell(1, 3).ch, 'a');
        assert_eq!(s.pos(), 4);
    }

    #[test]
    fn breites_zeichen_am_rechten_rand_bricht_ganz_um() {
        let mut s = TextScreen::new();
        s.print(&"x".repeat(79)); // Cursor auf Spalte 80
        assert_eq!(s.pos(), 80);
        s.print("中");
        // Letzte Spalte bleibt leer, das Zeichen steht in Zeile 2.
        assert_eq!(s.cell(1, 80).ch, ' ');
        assert_eq!(s.cell(2, 1).ch, '中');
        assert!(s.cell(2, 2).fortsetzung);
    }

    #[test]
    fn ueberschreiben_einer_haelfte_leert_die_andere() {
        let mut s = TextScreen::new();
        s.print("中");
        // Auf den Anfang schreiben → Fortsetzung muss weg.
        s.locate(1, 1).unwrap();
        s.print("a");
        assert_eq!(s.cell(1, 1).ch, 'a');
        assert!(!s.cell(1, 2).fortsetzung);
        assert_eq!(s.cell(1, 2).ch, ' ');

        // Auf die Fortsetzung schreiben → Anfang muss weg.
        let mut s2 = TextScreen::new();
        s2.print("中");
        s2.locate(1, 2).unwrap(); // klemmt auf den Zeichenanfang
        assert_eq!(s2.pos(), 1);
        s2.locate(1, 3).unwrap();
        s2.print("b");
        assert_eq!(s2.cell(1, 3).ch, 'b');
    }

    #[test]
    fn locate_auf_fortsetzung_trifft_den_zeichenanfang() {
        let mut s = TextScreen::new();
        s.print("中");
        s.locate(1, 2).unwrap();
        assert_eq!(s.pos(), 1);
    }

    #[test]
    fn rueckschritt_loescht_ein_breites_zeichen_ganz() {
        let mut s = TextScreen::new();
        s.print("a中");
        assert_eq!(s.pos(), 4);
        s.rueckschritt();
        assert_eq!(s.pos(), 2);
        assert_eq!(s.cell(1, 2).ch, ' ');
        assert!(!s.cell(1, 2).fortsetzung);
        assert_eq!(s.cell(1, 3).ch, ' ');
    }

    #[test]
    fn umbruch_am_rechten_rand() {
        let mut s = TextScreen::new();
        s.print(&"x".repeat(81));
        assert_eq!(s.cell(1, 80).ch, 'x');
        assert_eq!(s.cell(2, 1).ch, 'x');
        assert_eq!((s.csrlin(), s.pos()), (2, 2));
    }

    #[test]
    fn scrollen_am_unteren_rand() {
        let mut s = TextScreen::new();
        s.locate(25, 1).unwrap();
        s.print("unten\n"); // erzwingt Scroll
        assert_eq!(s.cell(24, 1).ch, 'u');
        assert_eq!(s.cell(25, 1).ch, ' ');
        assert_eq!(s.csrlin(), 25);
    }

    #[test]
    fn view_print_begrenzt_scrollen() {
        let mut s = TextScreen::new();
        s.locate(1, 1).unwrap();
        s.print("kopf");
        s.view_print(3, 5).unwrap();
        s.locate(5, 1).unwrap();
        s.print("a\nb"); // scrollt nur Zeilen 3–5
        assert_eq!(s.cell(1, 1).ch, 'k');
        assert_eq!(s.cell(4, 1).ch, 'a');
        assert_eq!(s.cell(5, 1).ch, 'b');
    }

    #[test]
    fn locate_prueft_grenzen() {
        let mut s = TextScreen::new();
        assert!(s.locate(0, 1).is_err());
        assert!(s.locate(26, 1).is_err());
        assert!(s.locate(25, 80).is_ok());
    }

    #[test]
    fn dynamische_groesse_und_resize() {
        let mut s = TextScreen::with_size(120, 40);
        assert_eq!((s.cols(), s.rows()), (120, 40));
        s.locate(40, 1).unwrap();
        s.print("!");
        assert_eq!(s.cell(40, 1).ch, '!');

        // Verkleinern: Inhalt oben links bleibt, Cursor wird geklemmt
        s.locate(1, 1).unwrap();
        s.print("K");
        s.resize(100, 30);
        assert_eq!((s.cols(), s.rows()), (100, 30));
        assert_eq!(s.cell(1, 1).ch, 'K');

        // Unter Minimum wird auf 80×25 geklemmt
        s.resize(10, 5);
        assert_eq!((s.cols(), s.rows()), (80, 25));
    }

    #[test]
    fn blink_wird_als_hell_simuliert() {
        let mut s = TextScreen::new();
        s.set_color(17, 0); // blinkend Blau → helles Blau
        assert_eq!(s.fg, 9);
        s.set_color(31, 0); // blinkend Hellweiß → Hellweiß
        assert_eq!(s.fg, 15);
        s.set_color(7, 0);
        assert_eq!(s.fg, 7);
    }

    #[test]
    fn resize_klemmt_cursor_und_scrollbereich() {
        let mut s = TextScreen::with_size(120, 40);
        s.locate(1, 1).unwrap();
        s.print("K");
        s.view_print(30, 38).unwrap();
        s.locate(38, 5).unwrap();

        s.resize(80, 25);
        // Inhalt oben links bleibt.
        assert_eq!(s.cell(1, 1).ch, 'K');
        // Cursor liegt in den neuen Grenzen.
        assert!(s.csrlin() <= s.rows() && s.pos() <= s.cols());
        // Der Bereich lag ganz außerhalb → voller Bildschirm.
        s.locate(25, 1).unwrap();
        s.print("u\n");
        assert_eq!(s.cell(24, 1).ch, 'u');
    }

    #[test]
    fn resize_klemmt_nur_den_unteren_rand_wenn_der_bereich_bleibt() {
        let mut s = TextScreen::with_size(100, 40);
        s.view_print(5, 38).unwrap();
        s.resize(100, 30);
        // Oberer Rand bleibt bei 5, unterer klemmt auf 30.
        s.locate(30, 1).unwrap();
        s.print("a\nb");
        assert_eq!(s.cell(29, 1).ch, 'a');
        assert_eq!(s.cell(30, 1).ch, 'b');
        // Zeile 4 liegt außerhalb und bleibt unberührt.
        assert_eq!(s.cell(4, 1).ch, ' ');
    }

    #[test]
    fn resize_folgt_vollem_scrollbereich() {
        let mut s = TextScreen::new();
        s.resize(90, 40);
        s.locate(40, 1).unwrap();
        s.print("a\nb"); // Scroll am neuen unteren Rand
        assert_eq!(s.cell(39, 1).ch, 'a');
        assert_eq!(s.cell(40, 1).ch, 'b');
    }
}
