// Experiment log: PNG vision readability
//
// font8x8 (8x8 bitmap, 1-bit):
//   pad=0: words readable, digits unreliable.
//     Sonnet misreads PIDs, load averages, memory.
//   pad=1: same digit problems.
//   pad=2 (1080x880): Sonnet reads uptime but not
//     load avg (answered 0.00 vs 1.62) or memory.
//     Opus reads uptime but misidentifies PID column
//     (answered PRI value instead of PID).
//   Conclusion: 8x8 too small for digit precision.
//
// noto-sans-mono-bitmap Size20 (10x20, anti-aliased):
//   pad=2 (1320x880, ~1565 tok/q): all agree (Opus).
//     Sonnet: uptime+load agree, memory disagrees
//     (14.46 vs 14.4G — G in progress bar ambiguous).
//   pad=0 (1080x800, ~1160 tok/q): all agree (Sonnet).
//     Text/PNG ratio 1.13x. Good sweet spot.
//
// noto-sans-mono-bitmap Size16 (8x16, anti-aliased):
//   pad=0 (960x640, ~720 tok/q): Sonnet misreads
//     uptime (04:30:25 vs 04:50:25, confuses 5/3).
//   pad=1 (960x680, ~900 tok/q): all agree (Sonnet).
//     Text/PNG ratio 1.45x.
//   pad=1, scale=90 (864x612, ~710 tok/q): all agree
//     (Sonnet). Text/PNG ratio 1.87x.
//   pad=1, scale=80, 120col (768x544, ~590 tok/q):
//     all agree (Sonnet). Text/PNG ratio 2.20x.
//   pad=1, scale=80, 80col (576x544, ~409 tok/q):
//     all agree (Sonnet). Text/PNG ratio 3.17x.
//     Current winner.
//
// noto-sans-mono-bitmap Size20 (10x20) downscaled:
//   pad=1, scale=65, 80col (520x327, ~257 tok/q):
//     Load avg correct, uptime misreads 5→3.
//     Text/PNG ratio 2.89x.
//   pad=1, scale=70, 80col (616x352, ~289 tok/q):
//     all agree (Sonnet). Text/PNG ratio 2.61x.
//     New winner — fewer tokens than Size16+scale=80.
//   pad=1, scale=67, 80col (536x337, ~289 tok/q):
//     all agree (Sonnet). Text/PNG ratio 2.60x.
//   pad=1, scale=66, 80col (528x332, ~257 tok/q):
//     all agree (Sonnet). Text/PNG ratio 2.91x.
//     Current winner.
//   pad=1, scale=70 (672x476, ~437 tok/q): FAIL.
//     Sonnet misreads uptime (03:46 vs 05:05).
//     Load avg still correct. Text/PNG ratio 2.96x.
//
// font8x8 (8x8 bitmap, 1-bit) + Lanczos3 upscale:
//   pad=0, scale=120 (1152x384, ~449 tok/q): FAIL.
//     Total garbage — Lanczos blurs 1-bit pixels into
//     gray mush. Uptime: "1333 days", load: "14.96".
//
// font8x8 + nearest-neighbor upscale:
//   pad=0, scale=120 (1152x384, ~617 tok/q): partial.
//     Uptime correct, load avg wrong (1.93 vs 2.38).
//     Text/PNG ratio 2.09x.
//   pad=0, scale=130 (1248x416, ~704 tok/q): partial.
//     Load avg correct, uptime truncated ("4 days").
//     NN upscale. Text/PNG ratio 1.92x.
//   pad=0, scale=130 Lanczos3 (1248x416, ~704 tok/q):
//     Load avg correct, uptime off by 1s (race).
//     Text/PNG ratio 1.85x. Better than NN but still
//     not as reliable as noto Size16.
//   pad=1, scale=120 NN (1296x432, ~781 tok/q):
//     Load avg correct, uptime missing "4 days,"
//     prefix. Text/PNG ratio 1.64x.
//
// fontdue + JetBrains Mono 18pt (11x21, anti-aliased):
//   pad=1, scale=90 (792x475, ~618 tok/q): 5/5 table.
//     No l/1 confusion. Clean anti-aliasing.
//   pad=1, scale=85 (748x448, ~551 tok/q): 4/5 table.
//     l→1 confusion (ecrl→ecr1).
//   pad=1, scale=80 (704x422, ~506 tok/q): 3/5 table.
//     i→1 and r→n confusion.
//
// Model notes:
//   Opus: better at column identification but still
//     fails on dense process tables with 8x8 font.
//   Sonnet: cheaper, works well with Size16+pad=1
//     and Size20+pad=0. Skips highlighted rows
//     (treats them as headers).

mod common;

use base64::Engine;
use common::{
    request,
    request_binary,
    socket_path,
    start_daemon,
    wait_for_socket,
};
use rand::Rng;
use rand::RngExt;
use serde_json::Value;

const MODEL: &str = "claude-sonnet-4-20250514";

async fn ask_claude(
    api_key: &str,
    content: Vec<Value>,
) -> (String, u64) {
    let body = serde_json::json!({
        "model": MODEL,
        "max_tokens": 64,
        "messages": [{
            "role": "user",
            "content": content,
        }],
    });

    let client = reqwest::Client::new();
    let resp: Value = client
        .post(
            "https://api.anthropic.com/v1/messages",
        )
        .header("x-api-key", api_key)
        .header("anthropic-version", "2023-06-01")
        .header("content-type", "application/json")
        .json(&body)
        .send()
        .await
        .unwrap()
        .json()
        .await
        .unwrap();
    let answer = resp["content"][0]["text"]
        .as_str()
        .unwrap_or("")
        .trim()
        .to_string();
    let tokens = resp["usage"]["input_tokens"]
        .as_u64()
        .unwrap_or(0);
    (answer, tokens)
}

async fn ask_with_image(
    api_key: &str,
    png: &[u8],
    question: &str,
) -> (String, u64) {
    let b64 = base64::engine::general_purpose::STANDARD
        .encode(png);
    let content = vec![
        serde_json::json!({
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": "image/png",
                "data": b64,
            },
        }),
        serde_json::json!({
            "type": "text",
            "text": format!(
                "{question} \
                 Answer with ONLY the value, \
                 nothing else."
            ),
        }),
    ];
    ask_claude(api_key, content).await
}

async fn ask_with_text(
    api_key: &str,
    text: &str,
    question: &str,
) -> (String, u64) {
    let content = vec![serde_json::json!({
        "type": "text",
        "text": format!(
            "Here is a terminal screenshot as text:\n\
             ```\n{text}\n```\n\n\
             {question} \
             Answer with ONLY the value, nothing else."
        ),
    })];
    ask_claude(api_key, content).await
}

#[tokio::test]
#[ignore]
async fn htop_vision_vs_text() {
    let api_key =
        match std::env::var("ANTHROPIC_API_KEY") {
            Ok(k) if !k.is_empty() => k,
            _ => {
                eprintln!(
                    "ANTHROPIC_API_KEY not set, skipping"
                );
                return;
            }
        };

    if std::process::Command::new("htop")
        .arg("--version")
        .output()
        .is_err()
    {
        eprintln!("htop not installed, skipping");
        return;
    }

    let sock = socket_path();
    let _ = std::fs::remove_file(&sock);
    let mut daemon = start_daemon(&sock);
    wait_for_socket(&sock).await;

    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "create",
            "cmd_args": ["/bin/sh"],
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);
    let id = resp["id"].as_str().unwrap().to_string();

    // Launch htop with huge delay so it won't refresh
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "send",
            "id": id,
            "input": "htop --delay=10000\n",
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);

    // Let htop do its initial render
    tokio::time::sleep(
        std::time::Duration::from_secs(3),
    )
    .await;

    // Capture PNG
    let (header, png) = request_binary(
        &sock,
        &serde_json::json!({
            "cmd": "screenshot",
            "id": id,
        }),
    )
    .await;
    assert_eq!(header["ok"], true);

    let img_path =
        std::env::temp_dir().join("t4a_htop.png");
    std::fs::write(&img_path, &png).unwrap();
    eprintln!(
        "\nPNG saved to: {}",
        img_path.display()
    );

    // Capture text
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "text",
            "id": id,
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);
    let lines = resp["lines"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|l| l.as_str())
        .collect::<Vec<_>>()
        .join("\n");

    eprintln!(
        "\n=== htop text ===\n{lines}\n=== end ===\n"
    );

    let questions = [
        "What is the uptime?",
        "What is the 1-minute load average?",
    ];

    let mut total_png_tokens: u64 = 0;
    let mut total_text_tokens: u64 = 0;
    let mut failures = Vec::new();

    for q in &questions {
        let (png_ans, png_tok) =
            ask_with_image(&api_key, &png, q).await;
        let (text_ans, text_tok) =
            ask_with_text(&api_key, &lines, q).await;

        let agree = png_ans == text_ans;
        eprintln!("\nQ: {q}");
        eprintln!(
            "  PNG:  {png_ans} ({png_tok} input tokens)"
        );
        eprintln!(
            "  Text: {text_ans} \
             ({text_tok} input tokens)"
        );
        eprintln!("  Agreement: {agree}");

        assert!(
            !png_ans.is_empty(),
            "empty PNG answer"
        );
        assert!(
            !text_ans.is_empty(),
            "empty text answer"
        );
        if !agree {
            failures.push(format!(
                "Q: {q}\n  PNG={png_ans}\n  \
                 Text={text_ans}"
            ));
        }

        total_png_tokens += png_tok;
        total_text_tokens += text_tok;
    }

    let ratio = total_text_tokens as f64
        / total_png_tokens as f64;
    eprintln!("\n=== Token Summary ===");
    eprintln!("PNG total:  {total_png_tokens} tokens");
    eprintln!(
        "Text total: {total_text_tokens} tokens"
    );
    eprintln!("Text/PNG ratio: {ratio:.2}x");

    // Cleanup
    let _ = request(
        &sock,
        &serde_json::json!({
            "cmd": "send",
            "id": id,
            "input": "q",
        }),
    )
    .await;
    tokio::time::sleep(
        std::time::Duration::from_millis(200),
    )
    .await;
    let _ = request(
        &sock,
        &serde_json::json!({
            "cmd": "kill",
            "id": id,
        }),
    )
    .await;
    let _ = daemon.kill();
    let _ = daemon.wait();
    let _ = std::fs::remove_file(&sock);

    assert!(
        failures.is_empty(),
        "PNG and text answers disagreed:\n{}",
        failures.join("\n"),
    );
}

fn random_cell(rng: &mut impl Rng) -> String {
    const CHARS: &[u8] =
        b"0123456789abcdefghijklmnopqrstuvwxyz\
          ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    (0..4)
        .map(|_| {
            CHARS[rng.random_range(0usize..CHARS.len())]
                as char
        })
        .collect()
}

fn build_table(
    rows: usize,
    cols: usize,
    rng: &mut impl Rng,
) -> (String, Vec<Vec<String>>) {
    let mut grid = Vec::new();
    let mut output = String::new();

    output.push_str("      ");
    for c in 0..cols {
        let label = (b'A' + c as u8) as char;
        if c > 0 {
            output.push_str(" | ");
        }
        output.push_str(&format!("{label:^4}"));
    }
    output.push('\n');

    for r in 0..rows {
        let mut row_cells = Vec::new();
        output.push_str(&format!("row={}  ", r + 1));
        for c in 0..cols {
            let cell = random_cell(rng);
            if c > 0 {
                output.push_str(" | ");
            }
            output.push_str(&cell);
            row_cells.push(cell);
        }
        output.push('\n');
        grid.push(row_cells);
    }
    (output, grid)
}

#[tokio::test]
#[ignore]
async fn table_readback() {
    let api_key =
        match std::env::var("ANTHROPIC_API_KEY") {
            Ok(k) if !k.is_empty() => k,
            _ => {
                eprintln!(
                    "ANTHROPIC_API_KEY not set, skipping"
                );
                return;
            }
        };

    let sock = socket_path();
    let _ = std::fs::remove_file(&sock);
    let mut daemon = start_daemon(&sock);
    wait_for_socket(&sock).await;

    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "create",
            "cmd_args": ["/bin/sh"],
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);
    let id = resp["id"].as_str().unwrap().to_string();

    let mut rng = rand::rng();
    let (table, grid) = build_table(5, 5, &mut rng);

    let cmd = format!(
        "clear; cat <<'TABLE'\n{table}TABLE\n"
    );
    let resp = request(
        &sock,
        &serde_json::json!({
            "cmd": "send",
            "id": id,
            "input": cmd,
        }),
    )
    .await;
    assert_eq!(resp["ok"], true);

    tokio::time::sleep(
        std::time::Duration::from_millis(500),
    )
    .await;

    // Capture PNG
    let (header, png) = request_binary(
        &sock,
        &serde_json::json!({
            "cmd": "screenshot",
            "id": id,
        }),
    )
    .await;
    assert_eq!(header["ok"], true);

    let img_path =
        std::env::temp_dir().join("t4a_table.png");
    std::fs::write(&img_path, &png).unwrap();
    eprintln!(
        "\nPNG saved to: {}",
        img_path.display()
    );
    eprintln!(
        "\n=== table ===\n{table}=== end ===\n"
    );

    let probes: Vec<(usize, usize, &str)> = vec![
        (0, 0, "top-left"),
        (0, 4, "top-right"),
        (4, 0, "bottom-left"),
        (4, 4, "bottom-right"),
        (2, 2, "center"),
    ];

    let mut total_tokens: u64 = 0;
    let mut total_dist: usize = 0;
    let mut total_chars: usize = 0;

    for (r, c, label) in &probes {
        let expected = &grid[*r][*c];
        let clbl = (b'A' + *c as u8) as char;
        let question = format!(
            "What is the 4-character value at \
             row={}, col={clbl}?",
            r + 1,
        );

        let (answer, tokens) =
            ask_with_image(&api_key, &png, &question)
                .await;

        let dist =
            levenshtein::levenshtein(expected, &answer);
        eprintln!(
            "{label} (r{},c{clbl}): \
             expected={expected} got={answer} \
             dist={dist} ({tokens} tok)",
            r + 1,
        );
        total_tokens += tokens;
        total_dist += dist;
        total_chars += expected.len();
    }

    let accuracy = 1.0
        - total_dist as f64 / total_chars as f64;
    eprintln!(
        "\nTotal PNG tokens: {total_tokens}"
    );
    eprintln!(
        "Accuracy: {:.0}% ({total_dist} edits \
         across {total_chars} chars)",
        accuracy * 100.0,
    );

    // Cleanup
    let _ = request(
        &sock,
        &serde_json::json!({
            "cmd": "kill",
            "id": id,
        }),
    )
    .await;
    let _ = daemon.kill();
    let _ = daemon.wait();
    let _ = std::fs::remove_file(&sock);

    assert!(
        total_dist <= 10,
        "Vision total edit distance {total_dist} \
         exceeds threshold of 10 \
         ({total_chars} chars, {:.0}% accuracy)",
        accuracy * 100.0,
    );
}
