use farena::FileArena;
use farena::FileArenaWriter;
use farena::Location;

#[test]
fn write_and_read_single_file() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc1 = writer.push("hello").unwrap();
    let loc2 = writer.push(" world").unwrap();
    let loc3 = writer.push("!").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    assert_eq!(arena.get(loc1).unwrap(), b"hello");
    assert_eq!(arena.get(loc2).unwrap(), b" world");
    assert_eq!(arena.get(loc3).unwrap(), b"!");
}

#[test]
fn write_and_read_multiple_files() {
    let mut w1 = FileArenaWriter::new(0).unwrap();
    let loc1 = w1.push("from file one").unwrap();
    let f1 = w1.finish().unwrap();

    let mut w2 = FileArenaWriter::new(1).unwrap();
    let loc2 = w2.push("from file two").unwrap();
    let f2 = w2.finish().unwrap();

    let arena = FileArena::new(vec![f1, f2]).unwrap();
    assert_eq!(arena.get(loc1).unwrap(), b"from file one");
    assert_eq!(arena.get(loc2).unwrap(), b"from file two");
}

#[test]
fn get_into_appends_to_buffer() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc1 = writer.push("foo").unwrap();
    let loc2 = writer.push("bar").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();

    let mut buf = Vec::new();
    arena.get_into(loc1, &mut buf).unwrap();
    assert_eq!(buf, b"foo");

    arena.get_into(loc2, &mut buf).unwrap();
    assert_eq!(buf, b"foobar");
}

#[test]
fn empty_push_returns_zero_location() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc = writer.push("").unwrap();
    assert_eq!(loc.len(), 0);
    assert_eq!(loc.offset(), 0);
    assert!(writer.is_empty());

    // finish() always returns a file now — 0-byte for empty writer
    let file = writer.finish().unwrap();
    assert_eq!(file.metadata().unwrap().len(), 0);
}

#[test]
fn push_accepts_various_types() {
    let mut writer = FileArenaWriter::new(0).unwrap();

    let loc1 = writer.push("string slice").unwrap();
    let loc2 = writer.push(b"byte slice").unwrap();
    let loc3 = writer.push(String::from("owned string")).unwrap();
    let loc4 = writer.push(vec![1u8, 2, 3, 4, 5]).unwrap();

    let file = writer.finish().unwrap();
    let arena = FileArena::new(vec![file]).unwrap();

    assert_eq!(arena.get(loc1).unwrap(), b"string slice");
    assert_eq!(arena.get(loc2).unwrap(), b"byte slice");
    assert_eq!(arena.get(loc3).unwrap(), b"owned string");
    assert_eq!(arena.get(loc4).unwrap(), vec![1u8, 2, 3, 4, 5]);
}

#[test]
fn empty_get_returns_empty() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    writer.push("some data").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let empty_loc = farena::Location::new(0, 0, 0);
    assert_eq!(arena.get(empty_loc).unwrap(), Vec::<u8>::new());
}

#[test]
fn roundtrip_binary_data() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let data: Vec<u8> = vec![0x00, 0x01, 0xFF, 0xFE, 0x80, 0x7F];
    let loc = writer.push(&data[..]).unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    assert_eq!(arena.get(loc).unwrap(), data);
}

#[test]
fn get_into_appends_to_nonempty_buffer() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc = writer.push("xyz").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let mut buf = vec![1u8, 2, 3];
    arena.get_into(loc, &mut buf).unwrap();
    assert_eq!(buf, &[1, 2, 3, b'x', b'y', b'z']);
}

#[test]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn roundtrip_large_payload_exceeds_bufwriter_buffer() {
    let payload: Vec<u8> = (0..65_536).map(|i| (i % 256) as u8).collect();

    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc = writer.push(&payload[..]).unwrap();
    assert_eq!(loc.len(), 65_536);

    let file = writer.finish().unwrap();
    let arena = FileArena::new(vec![file]).unwrap();
    assert_eq!(arena.get(loc).unwrap(), payload);
}

#[test]
fn roundtrip_many_sequential_pushes() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let mut locations = Vec::new();

    for i in 0u32..1000 {
        let data = format!("entry-{i:04}");
        let loc = writer.push(&data).unwrap();
        locations.push((loc, data));
    }

    let file = writer.finish().unwrap();
    let arena = FileArena::new(vec![file]).unwrap();

    for (loc, expected) in &locations {
        assert_eq!(
            arena.get(*loc).unwrap(),
            expected.as_bytes(),
            "mismatch at offset {}",
            loc.offset()
        );
    }
}

#[test]
#[should_panic(expected = "index out of bounds")]
fn get_with_invalid_file_index_panics() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    writer.push("data").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let bad_loc = farena::Location::new(5, 0, 4);
    let _ = arena.get(bad_loc);
}

#[test]
fn get_past_end_of_file_returns_error() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    writer.push("short").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let bad_loc = farena::Location::new(0, 0, 9999);
    assert!(
        arena.get(bad_loc).is_err(),
        "reading past EOF should return an IO error"
    );
}

#[test]
fn file_index_is_preserved_in_location() {
    // Verify file_index in Location matches what was passed to new()
    let mut writer = FileArenaWriter::new(7).unwrap();
    let loc = writer.push("test").unwrap();
    assert_eq!(loc.file_index(), 7);
    writer.finish().unwrap();
}

#[test]
fn get_str_into_appends_utf8_to_string() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc1 = writer.push("hello").unwrap();
    let loc2 = writer.push(" world").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let mut out = String::new();
    arena.get_str_into(loc1, &mut out).unwrap();
    arena.get_str_into(loc2, &mut out).unwrap();
    assert_eq!(out, "hello world");
}

#[test]
fn get_str_into_appends_to_nonempty_string() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc = writer.push("suffix").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let mut out = String::from("prefix-");
    arena.get_str_into(loc, &mut out).unwrap();
    assert_eq!(out, "prefix-suffix");
}

#[test]
fn get_str_into_empty_location_is_noop() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    writer.push("data").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let mut out = String::from("unchanged");
    arena
        .get_str_into(Location::new(0, 0, 0), &mut out)
        .unwrap();
    assert_eq!(out, "unchanged");
}

#[test]
fn get_str_into_preserves_prior_capacity() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc = writer.push("abc").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let mut out = String::with_capacity(64);
    let ptr_before = out.as_ptr();
    arena.get_str_into(loc, &mut out).unwrap();
    // Should not have reallocated — capacity was sufficient
    assert_eq!(out.as_ptr(), ptr_before);
}

#[test]
fn get_str_into_handles_valid_utf8() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc = writer.push("abc").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();

    // Verify get returns correct data
    assert_eq!(arena.get(loc).unwrap(), b"abc");

    let mut out = String::new();
    arena.get_str_into(loc, &mut out).unwrap();
    assert_eq!(out, "abc");
}

#[test]
fn get_str_into_handles_appended_utf8() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc1 = writer.push("hello").unwrap();
    let loc2 = writer.push(" world").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let mut out = String::new();
    arena.get_str_into(loc1, &mut out).unwrap();
    arena.get_str_into(loc2, &mut out).unwrap();
    assert_eq!(out, "hello world");
}

#[test]
fn get_str_into_handles_nonempty_string_input() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc = writer.push("suffix").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let mut out = String::from("prefix-");
    arena.get_str_into(loc, &mut out).unwrap();
    assert_eq!(out, "prefix-suffix");
}

#[test]
fn get_str_into_validates_utf8_and_appends() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc = writer.push("abc").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let mut out = String::new();
    arena.get_str_into(loc, &mut out).unwrap();
    assert_eq!(out, "abc");
}

#[test]
fn into_arena_single_writer_roundtrip() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    let loc1 = writer.push("alpha").unwrap();
    let loc2 = writer.push("beta").unwrap();
    let arena = writer.into_arena().unwrap();

    assert_eq!(arena.get(loc1).unwrap(), b"alpha");
    assert_eq!(arena.get(loc2).unwrap(), b"beta");
}

#[test]
fn into_arena_empty_writer_is_valid() {
    // Even with nothing written, into_arena produces a usable (empty) arena
    let writer = FileArenaWriter::new(0).unwrap();
    let arena = writer.into_arena().unwrap();
    assert_eq!(arena.file_count(), 1);
    // A zero-length Location into the empty arena returns empty bytes
    assert_eq!(arena.get(Location::new(0, 0, 0)).unwrap(), b"");
}

#[test]
fn new_with_empty_vec_returns_error() {
    // Creating an arena with no files should fail immediately
    let result = FileArena::new(vec![]);
    assert!(result.is_err());
    match result {
        Err(e) => assert_eq!(e.to_string(), "FileArena::new: files vector is empty"),
        Ok(_) => panic!("Expected error, got Ok"),
    }
}

#[test]
fn new_with_nonempty_vec_succeeds() {
    // Creating an arena with one file should succeed
    let mut writer = FileArenaWriter::new(0).unwrap();
    writer.push("data").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]);
    assert!(arena.is_ok());
    assert_eq!(arena.unwrap().file_count(), 1);
}

#[test]
fn get_str_into_rejects_invalid_utf8() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    // Push invalid UTF-8 bytes
    let loc = writer.push([0xFF, 0xFE, 0x80, 0x81]).unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let mut out = String::new();

    assert!(arena.get_str_into(loc, &mut out).is_err());
    assert_eq!(out, "");
}

#[test]
fn get_into_restores_buffer_on_error() {
    let mut writer = FileArenaWriter::new(0).unwrap();
    writer.push("short").unwrap();
    let file = writer.finish().unwrap();

    let arena = FileArena::new(vec![file]).unwrap();
    let bad_loc = Location::new(0, 0, 9999);

    // Vec with existing content before the error
    let mut buf = b"prefix"[..].to_vec();
    let original_bytes = buf.clone();
    let original_len = buf.len();

    // get_into should fail and restore the buffer to its pre-call state
    assert!(arena.get_into(bad_loc, &mut buf).is_err());

    // Buffer length and content should be unchanged (no uninitialized bytes exposed)
    assert_eq!(buf.len(), original_len);
    assert_eq!(buf, original_bytes);
}
