use super::*;

#[derive(Clone)]
struct TestField {
    raw: Vec<u8>,
    name: Vec<u8>,
    value: Vec<u8>,
}

impl TestField {
    fn new(name: Vec<u8>, value: Vec<u8>) -> Self {
        let mut raw = Vec::with_capacity(name.len() + 1 + value.len());
        raw.extend_from_slice(&name);
        raw.push(b'=');
        raw.extend_from_slice(&value);
        Self { raw, name, value }
    }

    fn from_str(name: &str, value: impl AsRef<[u8]>) -> Self {
        Self::new(name.as_bytes().to_vec(), value.as_ref().to_vec())
    }

    fn structured(&self) -> StructuredField<'_> {
        StructuredField::new(&self.name, &self.value)
    }
}

fn make_raw_structured_identity_rows(rows: usize) -> Vec<Vec<TestField>> {
    let mut all = Vec::with_capacity(rows);
    for row in 0..rows {
        let mut fields = Vec::with_capacity(18);
        fields.push(TestField::from_str("TEST_ID", "raw-structured-identity"));
        fields.push(TestField::from_str("PERF_PROFILE", "mixed-cardinality"));
        fields.push(TestField::from_str("EMPTY_VALUE", b""));
        fields.push(TestField::from_str(
            "BINARY_VALUE",
            [0, b'=', (row & 0xff) as u8, 0xff],
        ));

        for offset in 0..6 {
            fields.push(TestField::from_str(
                &format!("LOW_CARD_{offset:02}"),
                format!("low-{offset:02}-{:02}", row % 16),
            ));
        }
        for offset in 0..4 {
            fields.push(TestField::from_str(
                &format!("MED_CARD_{offset:02}"),
                format!("medium-{offset:02}-{:04}", row % 257),
            ));
        }
        for offset in 0..4 {
            fields.push(TestField::from_str(
                &format!("HIGH_CARD_{offset:02}"),
                format!("high-{offset:02}-{row:06}"),
            ));
        }
        all.push(fields);
    }
    all
}

fn write_rows_raw_test_journal(
    path: &Path,
    rows: &[Vec<TestField>],
    options: EntryWriteOptions,
) -> Vec<u8> {
    let repo_file =
        crate::repository::File::from_path(path).expect("test journal path should parse");
    let mut journal_file = JournalFile::create(
        &repo_file,
        JournalFileOptions::new(test_uuid(1), test_uuid(2), test_uuid(3))
            .with_file_id(test_uuid(5)),
    )
    .expect("create raw corpus journal");
    let mut writer = JournalWriter::new(&mut journal_file, 1, test_uuid(4)).expect("create writer");
    let mut entry_fields = Vec::with_capacity(32);
    for (index, row) in rows.iter().enumerate() {
        entry_fields.clear();
        entry_fields.extend(
            row.iter()
                .map(|field| EntryField::raw(field.raw.as_slice())),
        );
        writer
            .add_entry_fields_with_options(
                &mut journal_file,
                entry_fields.iter().copied(),
                1_700_000_060_000_000 + index as u64 * 500,
                100 + index as u64 * 50,
                options,
            )
            .expect("write raw corpus entry");
    }
    journal_file.sync().expect("sync raw corpus journal");
    drop(journal_file);
    std::fs::read(path).expect("read raw corpus journal")
}

fn write_rows_structured_test_journal(
    path: &Path,
    rows: &[Vec<TestField>],
    options: EntryWriteOptions,
) -> Vec<u8> {
    let repo_file =
        crate::repository::File::from_path(path).expect("test journal path should parse");
    let mut journal_file = JournalFile::create(
        &repo_file,
        JournalFileOptions::new(test_uuid(1), test_uuid(2), test_uuid(3))
            .with_file_id(test_uuid(5)),
    )
    .expect("create structured corpus journal");
    let mut writer = JournalWriter::new(&mut journal_file, 1, test_uuid(4)).expect("create writer");
    let mut structured_fields = Vec::with_capacity(32);
    for (index, row) in rows.iter().enumerate() {
        structured_fields.clear();
        structured_fields.extend(row.iter().map(TestField::structured));
        writer
            .add_entry_structured_with_options(
                &mut journal_file,
                &structured_fields,
                1_700_000_060_000_000 + index as u64 * 500,
                100 + index as u64 * 50,
                options,
            )
            .expect("write structured corpus entry");
    }
    journal_file.sync().expect("sync structured corpus journal");
    drop(journal_file);
    std::fs::read(path).expect("read structured corpus journal")
}

fn write_rows_structured_with_live_mode(
    path: &Path,
    rows: &[Vec<TestField>],
    live_publish_every_entries: u64,
) -> Vec<u8> {
    let repo_file =
        crate::repository::File::from_path(path).expect("test journal path should parse");
    let mut journal_file = JournalFile::create(
        &repo_file,
        JournalFileOptions::new(test_uuid(1), test_uuid(2), test_uuid(3))
            .with_file_id(test_uuid(5)),
    )
    .expect("create live publication mode journal");
    let mut writer = JournalWriter::new(&mut journal_file, 1, test_uuid(4)).expect("create writer");
    writer.set_live_publish_every_entries(live_publish_every_entries);

    let mut structured_fields = Vec::with_capacity(32);
    for (index, row) in rows.iter().enumerate() {
        structured_fields.clear();
        structured_fields.extend(row.iter().map(TestField::structured));
        writer
            .add_entry_structured_with_options(
                &mut journal_file,
                &structured_fields,
                1_700_000_060_000_000 + index as u64 * 500,
                100 + index as u64 * 50,
                EntryWriteOptions::default().trusted_unique_payloads(true),
            )
            .expect("write live publication mode entry");
    }
    journal_file
        .sync()
        .expect("sync live publication mode journal");
    drop(journal_file);
    std::fs::read(path).expect("read live publication mode journal")
}

#[test]
fn structured_writer_matches_raw_payload_writer_bytes() {
    let dir = TempDir::new().expect("create temp dir");
    let journal_dir = dir.path().join("journals");
    std::fs::create_dir_all(&journal_dir).expect("create journal dir");
    let raw_path = journal_dir.join("raw.journal");
    let structured_path = journal_dir.join("structured.journal");

    let raw_fields = [
        b"MESSAGE=structured parity".as_slice(),
        b"PRIORITY=6".as_slice(),
        b"BINARY=\x00=\x01\xfe\xff".as_slice(),
    ];
    let structured_fields = [
        StructuredField::new(b"MESSAGE", b"structured parity"),
        StructuredField::new(b"PRIORITY", b"6"),
        StructuredField::new(b"BINARY", b"\x00=\x01\xfe\xff"),
    ];

    let raw_bytes = write_raw_test_journal(&raw_path, &raw_fields);
    let structured_bytes = write_structured_test_journal(
        &structured_path,
        &structured_fields,
        EntryWriteOptions::default(),
    );

    assert_eq!(structured_bytes, raw_bytes);

    if !journalctl_available() {
        eprintln!("journalctl not available; skipping structured stock verify");
        return;
    }
    let output = Command::new("journalctl")
        .arg("--verify")
        .arg("--file")
        .arg(&structured_path)
        .output()
        .expect("run journalctl verify");
    assert!(
        output.status.success(),
        "journalctl verify failed for structured file: stdout={} stderr={}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
fn mixed_entry_fields_match_raw_payload_writer_bytes() {
    let dir = TempDir::new().expect("create temp dir");
    let journal_dir = dir.path().join("journals");
    std::fs::create_dir_all(&journal_dir).expect("create journal dir");
    let raw_path = journal_dir.join("raw.journal");
    let mixed_path = journal_dir.join("mixed.journal");

    let raw_fields = [
        EntryField::raw(b"MESSAGE=mixed entry"),
        EntryField::raw(b"PRIORITY=6"),
        EntryField::raw(b"BINARY=\x00=\x01\xfe\xff"),
    ];
    let mixed_fields = [
        EntryField::raw(b"MESSAGE=mixed entry"),
        EntryField::structured(b"PRIORITY", b"6"),
        EntryField::structured(b"BINARY", b"\x00=\x01\xfe\xff"),
    ];

    let (raw_bytes, _, _) =
        write_entry_fields_test_journal(&raw_path, &raw_fields, EntryWriteOptions::default());
    let (mixed_bytes, _, _) =
        write_entry_fields_test_journal(&mixed_path, &mixed_fields, EntryWriteOptions::default());

    assert_eq!(mixed_bytes, raw_bytes);
}

#[test]
fn structured_writer_preserves_binary_field_values() {
    let dir = TempDir::new().expect("create temp dir");
    let journal_dir = dir.path().join("journals");
    std::fs::create_dir_all(&journal_dir).expect("create journal dir");
    let path = journal_dir.join("system.journal");
    let repo_file =
        crate::repository::File::from_path(&path).expect("test journal path should parse");

    let mut journal_file = JournalFile::create(
        &repo_file,
        JournalFileOptions::new(test_uuid(1), test_uuid(2), test_uuid(3)),
    )
    .expect("create journal");
    let mut writer = JournalWriter::new(&mut journal_file, 1, test_uuid(4)).expect("create writer");
    writer
        .add_entry_structured(
            &mut journal_file,
            &[
                StructuredField::new(b"MESSAGE", b"binary structured"),
                StructuredField::new(b"BINARY", b"\x00=\x01\xfe\xff"),
            ],
            1_700_000_060_000_000,
            100,
        )
        .expect("write structured binary entry");
    journal_file.sync().expect("sync journal");

    let mut entry_offsets = Vec::new();
    journal_file
        .entry_offsets(&mut entry_offsets)
        .expect("collect entry offsets");
    let payloads = journal_file
        .entry_data_objects(entry_offsets[0])
        .expect("entry data iterator")
        .map(|item| item.map(|object| object.raw_payload().to_vec()))
        .collect::<crate::error::Result<Vec<_>>>()
        .expect("read payloads");

    assert!(payloads.iter().any(|p| p == b"MESSAGE=binary structured"));
    assert!(payloads.iter().any(|p| p == b"BINARY=\x00=\x01\xfe\xff"));
}

#[test]
fn structured_writer_rejects_invalid_field_names_before_writing() {
    let dir = TempDir::new().expect("create temp dir");
    let journal_dir = dir.path().join("journals");
    std::fs::create_dir_all(&journal_dir).expect("create journal dir");
    let path = journal_dir.join("system.journal");
    let repo_file =
        crate::repository::File::from_path(&path).expect("test journal path should parse");

    let mut journal_file = JournalFile::create(
        &repo_file,
        JournalFileOptions::new(test_uuid(1), test_uuid(2), test_uuid(3)),
    )
    .expect("create journal");
    let mut writer = JournalWriter::new(&mut journal_file, 1, test_uuid(4)).expect("create writer");

    assert!(
        writer
            .add_entry_structured(
                &mut journal_file,
                &[StructuredField::new(b"not-valid", b"value")],
                1_700_000_060_000_000,
                100,
            )
            .is_err()
    );
    assert_eq!(journal_file.journal_header_ref().n_entries, 0);
}

#[test]
fn writer_field_name_policies_cover_journald_app_and_raw() {
    let dir = TempDir::new().expect("create temp dir");
    let journal_dir = dir.path().join("journals");
    std::fs::create_dir_all(&journal_dir).expect("create journal dir");
    assert_journald_policy(&journal_dir);
    assert_journal_app_policy(&journal_dir);
    assert_raw_policy(&journal_dir);
}

fn assert_journald_policy(journal_dir: &Path) {
    let journald_path = journal_dir.join("journald.journal");
    let (mut journal_file, mut writer) = create_policy_writer(&journald_path, [1, 2, 3], 4);
    writer
        .add_entry_structured(
            &mut journal_file,
            &[
                StructuredField::new(b"MESSAGE", b"trusted fields"),
                StructuredField::new(b"_HOSTNAME", b"synthetic-host"),
            ],
            1_700_002_111_000_000,
            1,
        )
        .expect("journald policy accepts protected fields");
    assert_eq!(journal_file.journal_header_ref().n_entries, 1);
}

fn assert_journal_app_policy(journal_dir: &Path) {
    let app_path = journal_dir.join("journal-app.journal");
    let (mut journal_file, mut writer) = create_policy_writer(&app_path, [5, 6, 7], 8);
    writer
        .add_entry_structured_with_options(
            &mut journal_file,
            &[
                StructuredField::new(b"MESSAGE", b"app valid"),
                StructuredField::new(b"_HOSTNAME", b"drop-host"),
                StructuredField::new(b"lowercase", b"drop-lowercase"),
            ],
            1_700_002_112_000_000,
            1,
            EntryWriteOptions::default().field_name_policy(FieldNamePolicy::JournalApp),
        )
        .expect("journal-app policy drops invalid fields");
    assert_eq!(journal_file.journal_header_ref().n_entries, 1);
    let payloads = first_entry_payloads(&journal_file, "journal-app");
    assert!(payloads.iter().any(|p| p == b"MESSAGE=app valid"));
    assert!(!payloads.iter().any(|p| p.starts_with(b"_HOSTNAME=")));
    assert!(!payloads.iter().any(|p| p.starts_with(b"lowercase=")));
    assert!(
        writer
            .add_entry_structured_with_options(
                &mut journal_file,
                &[StructuredField::new(b"_HOSTNAME", b"drop-only")],
                1_700_002_112_000_001,
                2,
                EntryWriteOptions::default().field_name_policy(FieldNamePolicy::JournalApp),
            )
            .is_err()
    );
}

fn assert_raw_policy(journal_dir: &Path) {
    let raw_path = journal_dir.join("raw.journal");
    let (mut journal_file, mut writer) = create_policy_writer(&raw_path, [9, 10, 11], 12);
    let long_name = vec![b'a'; 1024];
    writer
        .add_entry_fields_with_options(
            &mut journal_file,
            [
                EntryField::structured(b"lowercase", b"ok"),
                EntryField::structured(b"foo.bar", b"dot"),
                EntryField::structured(b"field name", b"space"),
                EntryField::structured(long_name.as_slice(), b"long"),
                EntryField::structured(b"BINARY", b"a\0=b"),
            ],
            1_700_002_113_000_000,
            1,
            EntryWriteOptions::default().field_name_policy(FieldNamePolicy::Raw),
        )
        .expect("raw policy accepts structure-only names");
    assert_raw_policy_payloads(&journal_file);
    assert!(
        writer
            .add_entry_fields_with_options(
                &mut journal_file,
                [EntryField::structured(b"BAD=NAME", b"bad")],
                1_700_002_113_000_001,
                2,
                EntryWriteOptions::default().field_name_policy(FieldNamePolicy::Raw),
            )
            .is_err()
    );
}

fn create_policy_writer(
    path: &Path,
    file_seeds: [u8; 3],
    boot_seed: u8,
) -> (JournalFile<MmapMut>, JournalWriter) {
    let repo_file = crate::repository::File::from_path(path).expect("test journal path");
    let mut journal_file = JournalFile::create(
        &repo_file,
        JournalFileOptions::new(
            test_uuid(file_seeds[0]),
            test_uuid(file_seeds[1]),
            test_uuid(file_seeds[2]),
        ),
    )
    .expect("create journal");
    let writer =
        JournalWriter::new(&mut journal_file, 1, test_uuid(boot_seed)).expect("create writer");
    (journal_file, writer)
}

fn first_entry_payloads(journal_file: &JournalFile<MmapMut>, label: &str) -> Vec<Vec<u8>> {
    let label = label.to_string();
    let mut entry_offsets = Vec::new();
    journal_file
        .entry_offsets(&mut entry_offsets)
        .unwrap_or_else(|_| panic!("collect {} entry offsets", label));
    journal_file
        .entry_data_objects(entry_offsets[0])
        .unwrap_or_else(|_| panic!("{} entry data iterator", label))
        .map(|item| item.map(|object| object.raw_payload().to_vec()))
        .collect::<crate::error::Result<Vec<_>>>()
        .unwrap_or_else(|_| panic!("read {} payloads", label))
}

fn assert_raw_policy_payloads(journal_file: &JournalFile<MmapMut>) {
    let payloads = first_entry_payloads(journal_file, "raw");
    assert!(payloads.iter().any(|p| p == b"lowercase=ok"));
    assert!(payloads.iter().any(|p| p == b"foo.bar=dot"));
    assert!(payloads.iter().any(|p| p == b"field name=space"));
    assert!(
        payloads
            .iter()
            .any(|p| p == &format!("{}=long", "a".repeat(1024)).into_bytes())
    );
    assert!(payloads.iter().any(|p| p == b"BINARY=a\0=b"));
}

#[test]
fn trusted_unique_payloads_keeps_unique_entry_output_identical() {
    let dir = TempDir::new().expect("create temp dir");
    let journal_dir = dir.path().join("journals");
    std::fs::create_dir_all(&journal_dir).expect("create journal dir");
    let default_path = journal_dir.join("default.journal");
    let trusted_path = journal_dir.join("trusted.journal");

    let fields = [
        StructuredField::new(b"MESSAGE", b"trusted unique"),
        StructuredField::new(b"PRIORITY", b"6"),
        StructuredField::new(b"SYSLOG_IDENTIFIER", b"journal-core-test"),
    ];
    let default_bytes =
        write_structured_test_journal(&default_path, &fields, EntryWriteOptions::default());
    let trusted_bytes = write_structured_test_journal(
        &trusted_path,
        &fields,
        EntryWriteOptions::default().trusted_unique_payloads(true),
    );

    assert_eq!(trusted_bytes, default_bytes);
}

#[test]
fn structured_writer_deduplicates_duplicate_payloads_by_default() {
    let dir = TempDir::new().expect("create temp dir");
    let journal_dir = dir.path().join("journals");
    std::fs::create_dir_all(&journal_dir).expect("create journal dir");
    let default_path = journal_dir.join("default.journal");
    let trusted_path = journal_dir.join("trusted.journal");

    let fields = [
        EntryField::structured(b"MESSAGE", b"duplicate"),
        EntryField::structured(b"MESSAGE", b"duplicate"),
        EntryField::structured(b"PRIORITY", b"6"),
    ];

    let (_, default_count, default_payloads) =
        write_entry_fields_test_journal(&default_path, &fields, EntryWriteOptions::default());
    assert_eq!(default_count, 2);
    assert_eq!(
        default_payloads
            .iter()
            .filter(|payload| payload.as_slice() == b"MESSAGE=duplicate")
            .count(),
        1
    );

    let (_, trusted_count, trusted_payloads) = write_entry_fields_test_journal(
        &trusted_path,
        &fields,
        EntryWriteOptions::default().trusted_unique_payloads(true),
    );
    assert_eq!(trusted_count, 3);
    assert_eq!(
        trusted_payloads
            .iter()
            .filter(|payload| payload.as_slice() == b"MESSAGE=duplicate")
            .count(),
        2
    );
}

#[test]
fn structured_writer_matches_raw_payload_writer_bytes_across_deterministic_corpus() {
    let dir = TempDir::new().expect("create temp dir");
    let journal_dir = dir.path().join("journals");
    std::fs::create_dir_all(&journal_dir).expect("create journal dir");
    let raw_path = journal_dir.join("raw.journal");
    let structured_path = journal_dir.join("structured.journal");
    let rows = make_raw_structured_identity_rows(512);
    let options = EntryWriteOptions::default().trusted_unique_payloads(true);

    let raw_bytes = write_rows_raw_test_journal(&raw_path, &rows, options);
    let structured_bytes = write_rows_structured_test_journal(&structured_path, &rows, options);

    assert_eq!(structured_bytes, raw_bytes);
}

#[test]
fn live_publication_modes_preserve_closed_file_bytes() {
    let dir = TempDir::new().expect("create temp dir");
    let journal_dir = dir.path().join("journals");
    std::fs::create_dir_all(&journal_dir).expect("create journal dir");
    let immediate_path = journal_dir.join("immediate.journal");
    let disabled_path = journal_dir.join("disabled.journal");
    let every_n_path = journal_dir.join("every-n.journal");
    let rows = make_raw_structured_identity_rows(65);

    let immediate_bytes = write_rows_structured_with_live_mode(&immediate_path, &rows, 1);
    let disabled_bytes = write_rows_structured_with_live_mode(&disabled_path, &rows, 0);
    let every_n_bytes = write_rows_structured_with_live_mode(&every_n_path, &rows, 8);

    assert_eq!(disabled_bytes, immediate_bytes);
    assert_eq!(every_n_bytes, immediate_bytes);
}
