//! Integration tests for dcmtk-core, porting DCMTK's ofstd test suite.
//!
//! Ported from:
//! - ofstd/tests/tuuid.cc (OFUUID tests → Uid generation uniqueness)
//! - ofstd/tests/tchrenc.cc (character encoding tests → charset module)
//! - ofstd/tests/terror.cc (error handling tests → DcmError)
//! - dcmdata/tests/tgenuid.cc (UID generation tests → Uid::generate)

use dicom_toolkit_core::charset::{decode_string, encode_string, DicomCharsetDecoder};
use dicom_toolkit_core::error::DcmError;
use dicom_toolkit_core::uid::Uid;
use std::collections::HashSet;

// ── Ported from dcmdata/tests/tgenuid.cc ────────────────────────────────────

/// Generates 10000 UIDs and verifies they are all unique.
/// Ports: dcmdata_generateUniqueIdentifier
#[test]
fn generated_uids_are_unique() {
    let mut seen = HashSet::new();
    for _ in 0..10_000 {
        let uid = Uid::generate("1.2.3").expect("UID generation must not fail");
        let s = uid.to_string();
        assert!(seen.insert(s.clone()), "Generated a duplicate UID: {s}");
    }
}

/// Generated UIDs conform to DICOM UID constraints.
/// Ports: dcmdata_generateUniqueIdentifier (extended)
#[test]
fn generated_uid_is_valid() {
    for _ in 0..100 {
        let uid = Uid::generate("1.2.840.10008").expect("UID generation must not fail");
        let s = uid.to_string();
        assert!(
            s.starts_with("1.2.840.10008."),
            "UID should start with root prefix"
        );
        assert!(s.len() <= 64, "UID exceeds max length 64: len={}", s.len());
        // must contain only digits and dots
        for ch in s.chars() {
            assert!(
                ch.is_ascii_digit() || ch == '.',
                "Invalid char '{ch}' in UID: {s}"
            );
        }
        // each component must not have a leading zero (except "0")
        for component in s.split('.') {
            if component.len() > 1 {
                assert!(
                    !component.starts_with('0'),
                    "Component with leading zero in UID: {s}"
                );
            }
        }
    }
}

// ── Ported from ofstd/tests/tuuid.cc ────────────────────────────────────────

/// Two generated UIDs are different; a UID equals itself.
/// Ports: ofstd_OFUUID_1
#[test]
fn uid_inequality_and_equality() {
    let a = Uid::generate("1.2.3").unwrap();
    let b = Uid::generate("1.2.3").unwrap();
    assert_ne!(a, b, "Two generated UIDs should differ");
    assert_eq!(a, a, "UID should equal itself");
    assert_eq!(b, b, "UID should equal itself");
}

// ── Ported from dcmdata/tests/tvrui.cc ──────────────────────────────────────

/// Valid UID strings are accepted.
/// Ports: dcmdata_uniqueIdentifier_1 (validation aspect)
#[test]
fn uid_accepts_valid_strings() {
    assert!(Uid::new("1.2.3.4").is_ok());
    assert!(Uid::new("1.2.840.10008.1.1").is_ok());
    assert!(Uid::new("0").is_ok());
    assert!(Uid::new("1.0.1").is_ok());
}

/// Invalid UID strings are rejected.
/// Ports: dcmdata_uniqueIdentifier validation checks
#[test]
fn uid_rejects_invalid_strings() {
    // empty
    assert!(Uid::new("").is_err(), "empty UID should be rejected");
    // leading dot
    assert!(
        Uid::new(".1.2.3").is_err(),
        "leading dot should be rejected"
    );
    // trailing dot
    assert!(
        Uid::new("1.2.3.").is_err(),
        "trailing dot should be rejected"
    );
    // consecutive dots
    assert!(
        Uid::new("1..2").is_err(),
        "consecutive dots should be rejected"
    );
    // non-numeric
    assert!(
        Uid::new("1.2.abc").is_err(),
        "non-numeric char should be rejected"
    );
    assert!(
        Uid::new("1.2.3a").is_err(),
        "non-numeric char should be rejected"
    );
    // leading zero in component
    assert!(
        Uid::new("1.02.3").is_err(),
        "leading zero in component should be rejected"
    );
    assert!(
        Uid::new("1.00.3").is_err(),
        "leading zeros in component should be rejected"
    );
}

/// UID exceeding 64 characters is rejected.
#[test]
fn uid_rejects_too_long() {
    // 65 character UID
    let _first = "1.".repeat(22) + "1"; // 1. × 22 + "1" = 45 + 20 = 65+ chars
    let long_uid = format!("1.{}", "2.".repeat(31) + "3"); // definitely > 64
    assert!(
        Uid::new(&long_uid).is_err(),
        "UID > 64 chars should be rejected"
    );
}

// ── Ported from ofstd/tests/tchrenc.cc ──────────────────────────────────────

/// Default charset decodes ASCII correctly.
/// Ports: character encoding basic tests
#[test]
fn charset_default_ascii_roundtrip() {
    let original = "Patient^Smith^John";
    let encoded = encode_string(original, "").unwrap();
    let decoded = decode_string(&encoded, "").unwrap();
    assert_eq!(decoded, original);
}

/// UTF-8 charset roundtrip.
#[test]
fn charset_utf8_roundtrip() {
    let original = "田中太郎"; // Japanese name
    let encoded = encode_string(original, "ISO_IR 192").unwrap();
    let decoded = decode_string(&encoded, "ISO_IR 192").unwrap();
    assert_eq!(decoded, original);
}

/// ISO-8859-1 (Latin-1) charset decodes correctly.
#[test]
fn charset_latin1_decode() {
    // "Müller" in ISO-8859-1: ü = 0xFC
    let bytes: Vec<u8> = vec![b'M', 0xFC, b'l', b'l', b'e', b'r'];
    // ISO_IR 100 = ISO-8859-2 in our mapping, but let's test what we mapped
    // The key thing: we support the DICOM term
    let result = decode_string(&bytes, "ISO_IR 100");
    assert!(result.is_ok(), "Latin-2 decode should succeed");
}

/// Unknown character set returns an error.
#[test]
fn charset_unknown_returns_error() {
    let result = decode_string(b"hello", "UNKNOWN_CHARSET_XYZ");
    assert!(result.is_err(), "Unknown charset should return error");
}

/// Multi-valued charset (backslash-separated terms) can be constructed.
#[test]
fn charset_multi_valued_decoder() {
    let decoder = DicomCharsetDecoder::new("ISO_IR 192").unwrap();
    let result = decoder.decode(b"Smith").unwrap();
    assert_eq!(result, "Smith");
}

// ── Ported from ofstd/tests/terror.cc ───────────────────────────────────────

/// DcmError variants have non-empty display strings.
/// Ports: error handling basic checks
#[test]
fn error_display_strings_are_non_empty() {
    let errors: Vec<DcmError> = vec![
        DcmError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "not found",
        )),
        DcmError::UnexpectedEof { offset: 42 },
        DcmError::InvalidFile {
            reason: "bad magic".into(),
        },
        DcmError::InvalidTag {
            group: 0x0008,
            element: 0x0010,
            reason: "test".into(),
        },
        DcmError::VrMismatch {
            group: 0x0010,
            element: 0x0010,
            expected: "PN".into(),
            found: "LO".into(),
        },
        DcmError::UnsupportedTransferSyntax {
            uid: "1.2.3".into(),
        },
        DcmError::NoCodec {
            uid: "1.2.3".into(),
        },
        DcmError::UnknownTag {
            group: 0x9999,
            element: 0x0001,
        },
        DcmError::InvalidUid {
            reason: "test".into(),
        },
        DcmError::CharsetError {
            reason: "test".into(),
        },
        DcmError::AssociationRejected {
            reason: "test".into(),
        },
        DcmError::DimseError {
            status: 0xC000,
            description: "test".into(),
        },
        DcmError::Timeout { seconds: 30 },
        DcmError::Other("test error".into()),
    ];

    for err in &errors {
        let display = err.to_string();
        assert!(
            !display.is_empty(),
            "Error display string should not be empty for {err:?}"
        );
    }
}

/// IO errors convert properly from std::io::Error.
#[test]
fn io_error_from_conversion() {
    let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
    let dcm_err: DcmError = io_err.into();
    assert!(matches!(dcm_err, DcmError::Io(_)));
    assert!(dcm_err.to_string().contains("I/O error"));
}
