//! Integration tests for the metrics HTTP server.
//!
//! These tests require `--features metrics`.

#![allow(clippy::unwrap_used)]

use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;

use tidaldb::TidalDb;

/// Make an HTTP GET request to the given address and path.
/// Returns (status_code, body).
fn http_get(addr: std::net::SocketAddr, path: &str) -> (u16, String) {
    let mut stream = TcpStream::connect(addr).expect("connect");
    write!(
        stream,
        "GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
    )
    .unwrap();
    stream.flush().unwrap();
    let mut response = String::new();
    stream.read_to_string(&mut response).unwrap();
    // Parse status line: "HTTP/1.1 200 OK"
    let status: u16 = response
        .lines()
        .next()
        .and_then(|l| l.split_whitespace().nth(1))
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);
    let body = response
        .splitn(2, "\r\n\r\n")
        .nth(1)
        .unwrap_or("")
        .to_string();
    (status, body)
}

fn open_with_metrics() -> (std::net::SocketAddr, TidalDb) {
    let db = TidalDb::builder()
        .ephemeral()
        .enable_metrics("127.0.0.1:0")
        .open()
        .expect("open should succeed");
    let addr = db
        .metrics_addr()
        .expect("metrics_addr should be Some when metrics enabled");
    (addr, db)
}

#[test]
fn metrics_server_starts_on_port_zero() {
    let (addr, db) = open_with_metrics();
    assert_ne!(addr.port(), 0, "OS should have assigned a real port");
    db.close().expect("close should succeed");
}

#[test]
fn healthz_returns_200_json() {
    let (addr, _db) = open_with_metrics();

    let (status, body) = http_get(addr, "/healthz");
    assert_eq!(status, 200);
    assert!(body.contains("\"status\":\"ok\""), "body: {body}");
    assert!(
        body.contains("\"uptime_seconds\":"),
        "body should have uptime: {body}"
    );
    assert!(
        body.contains("\"version\":"),
        "body should have version: {body}"
    );
    assert!(
        body.contains("\"build_hash\":"),
        "body should have build_hash: {body}"
    );
}

#[test]
fn metrics_returns_200_prometheus() {
    let (addr, _db) = open_with_metrics();

    let (status, body) = http_get(addr, "/metrics");
    assert_eq!(status, 200);
    assert!(
        body.contains("tidaldb_uptime_seconds"),
        "missing uptime metric in body: {body}"
    );
    assert!(
        body.contains("tidaldb_health_ok"),
        "missing health_ok metric in body: {body}"
    );
    assert!(
        body.contains("tidaldb_info"),
        "missing info metric in body: {body}"
    );
}

#[test]
fn uptime_increases_over_time() {
    let (addr, _db) = open_with_metrics();

    let (_, body1) = http_get(addr, "/metrics");
    std::thread::sleep(Duration::from_millis(150));
    let (_, body2) = http_get(addr, "/metrics");

    let uptime1 = extract_uptime(&body1);
    let uptime2 = extract_uptime(&body2);

    assert!(
        uptime2 > uptime1,
        "uptime should increase: {uptime1} -> {uptime2}"
    );
}

#[test]
fn health_ok_value_is_one() {
    let (addr, _db) = open_with_metrics();

    let (_, body) = http_get(addr, "/metrics");
    let health_line = body
        .lines()
        .find(|l| l.starts_with("tidaldb_health_ok{"))
        .expect("should have health_ok metric");
    assert!(
        health_line.ends_with(" 1"),
        "health should be 1: {health_line}"
    );
}

#[test]
fn not_found_returns_404() {
    let (addr, _db) = open_with_metrics();
    let (status, _) = http_get(addr, "/nonexistent");
    assert_eq!(status, 404);
}

#[test]
fn close_stops_server() {
    let (addr, db) = open_with_metrics();
    // Server should respond before close
    let (status, _) = http_get(addr, "/healthz");
    assert_eq!(status, 200);

    db.close().expect("close should succeed");

    // Give the thread a moment to shut down
    std::thread::sleep(Duration::from_millis(200));

    // After close, connection should be refused
    let result = TcpStream::connect_timeout(&addr.into(), Duration::from_millis(200));
    assert!(result.is_err(), "connection should be refused after close");
}

// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------

fn extract_uptime(prometheus_body: &str) -> f64 {
    for line in prometheus_body.lines() {
        if line.starts_with("tidaldb_uptime_seconds{") {
            let val = line.split_whitespace().last().unwrap_or("0");
            return val.parse().unwrap_or(0.0);
        }
    }
    0.0
}
