//! Structural tests for `#[derive(Payload)]`.
//!
//! These live in their own test crate rather than
//! `ktstr_test_macro.rs` to keep the struct-only `Payload`-derive
//! tests grouped and free of the `#[ktstr_test]` harness/`#[ctor]`
//! machinery. Plain `#[test]` functions are not hidden by a
//! `#[ktstr_test]`-carrying binary: the ctor's `--list` branch calls
//! `list_plain_tests` / `ktstr_list_only` (see
//! `src/test_support/dispatch.rs`), which surface plain `#[test]`
//! items to nextest.

use ktstr::test_support::{MetricCheck, OutputFormat, PayloadKind, Polarity};

/// Minimal derive: only `binary` is set, everything else defaults.
/// Checks the const-name strip + uppercase conversion,
/// name-falls-back-to-binary, and the ExitCode output default.
#[derive(ktstr::Payload)]
#[payload(binary = "fio")]
#[allow(dead_code)]
struct FioMinimalPayload;

#[test]
fn derive_payload_minimal_const_name_and_defaults() {
    assert_eq!(FIO_MINIMAL.name, "fio");
    assert!(matches!(FIO_MINIMAL.kind, PayloadKind::Binary("fio")));
    assert!(matches!(FIO_MINIMAL.output, OutputFormat::ExitCode));
    assert!(FIO_MINIMAL.default_args.is_empty());
    assert!(FIO_MINIMAL.default_checks.is_empty());
    assert!(FIO_MINIMAL.metrics.is_empty());
}

/// Full grammar: every optional attribute at once. Checks
/// accumulation of `default_args` across multiple attrs, MetricCheck
/// expressions, MetricHint with every polarity variant surface.
#[derive(ktstr::Payload)]
#[payload(
    binary = "fio",
    name = "fio_custom",
    output = Json,
)]
#[default_args("--output-format=json", "--minimal")]
#[default_args("--runtime=30")]
#[default_check(exit_code_eq(0))]
#[default_check(min("jobs.0.read.iops", 1000.0))]
#[metric(name = "jobs.0.read.iops", polarity = HigherBetter, unit = "iops")]
#[metric(name = "lat_ns", polarity = LowerBetter, unit = "ns")]
#[metric(name = "target_cpu", polarity = TargetValue(50.0), unit = "%")]
#[metric(name = "unlabeled")]
#[allow(dead_code)]
struct FioFullPayload;

#[test]
fn derive_payload_full_grammar() {
    assert_eq!(FIO_FULL.name, "fio_custom");
    assert!(matches!(FIO_FULL.kind, PayloadKind::Binary("fio")));
    assert!(matches!(FIO_FULL.output, OutputFormat::Json));

    assert_eq!(
        FIO_FULL.default_args,
        &["--output-format=json", "--minimal", "--runtime=30"],
    );

    assert_eq!(FIO_FULL.default_checks.len(), 2);
    assert!(matches!(
        FIO_FULL.default_checks[0],
        MetricCheck::ExitCodeEq(0)
    ));
    assert!(matches!(
        FIO_FULL.default_checks[1],
        MetricCheck::Min { metric, value } if metric == "jobs.0.read.iops" && value == 1000.0,
    ));

    assert_eq!(FIO_FULL.metrics.len(), 4);
    assert_eq!(FIO_FULL.metrics[0].name, "jobs.0.read.iops");
    assert_eq!(FIO_FULL.metrics[0].polarity, Polarity::HigherBetter);
    assert_eq!(FIO_FULL.metrics[0].unit, "iops");
    assert_eq!(FIO_FULL.metrics[1].name, "lat_ns");
    assert_eq!(FIO_FULL.metrics[1].polarity, Polarity::LowerBetter);
    assert_eq!(FIO_FULL.metrics[1].unit, "ns");
    assert_eq!(FIO_FULL.metrics[2].name, "target_cpu");
    assert_eq!(FIO_FULL.metrics[2].polarity, Polarity::TargetValue(50.0));
    assert_eq!(FIO_FULL.metrics[2].unit, "%");
    assert_eq!(FIO_FULL.metrics[3].name, "unlabeled");
    assert_eq!(FIO_FULL.metrics[3].polarity, Polarity::Unknown);
    assert_eq!(FIO_FULL.metrics[3].unit, "");
}

/// `TargetValue` accepts negative and integer-valued targets. A
/// negative literal parses as `Expr::Unary(Neg, Lit)` (never a bare
/// `Lit`) and an integer is `Lit::Int`; both coerce to f64. Pins the
/// derive fix — previously these were rejected with a misleading
/// "must be a float literal" error.
#[derive(ktstr::Payload)]
#[payload(binary = "tgt", output = Json)]
#[metric(name = "signed_delta", polarity = TargetValue(-1.5))]
#[metric(name = "zero_target", polarity = TargetValue(0))]
#[allow(dead_code)]
struct TargetValuePayload;

#[test]
fn derive_payload_target_value_negative_and_integer() {
    assert_eq!(TARGET_VALUE.metrics.len(), 2);
    assert_eq!(
        TARGET_VALUE.metrics[0].polarity,
        Polarity::TargetValue(-1.5)
    );
    assert_eq!(TARGET_VALUE.metrics[1].polarity, Polarity::TargetValue(0.0));
}

/// Struct name with NO `Payload` suffix: the derive converts the
/// full CamelCase ident to SCREAMING_SNAKE and uses that as the
/// const name. `StressNg` → `STRESS_NG`.
///
/// This test pins ONLY the emitted `const` identifier path. It
/// does NOT pin `Payload.name` — that field comes from the
/// `#[payload(...)]` attribute: here neither `name = "..."` nor a
/// short alias is supplied, so the `.name` field falls back to
/// the `binary` attribute (`"stress-ng"`), which happens to be
/// identical to the lowercased const name. That coincidence is
/// NOT the invariant under test; the const-identifier derivation
/// is. If a future const-name rule changed (e.g. to keep CamelCase
/// untouched), only line 1's assertion would break — line 2's
/// `Binary("stress-ng")` would still hold because it comes from
/// the attribute, not the ident. The two assertions exercise
/// different code paths that happen to produce matching strings
/// here; do not collapse them.
#[derive(ktstr::Payload)]
#[payload(binary = "stress-ng")]
#[allow(dead_code)]
struct StressNg;

#[test]
fn derive_payload_no_suffix_keeps_full_name() {
    assert_eq!(STRESS_NG.name, "stress-ng");
    assert!(matches!(STRESS_NG.kind, PayloadKind::Binary("stress-ng")));
}

/// Multi-word struct named with a `Payload` suffix. The suffix
/// strip happens BEFORE the SCREAMING_SNAKE conversion so the
/// generated const name is `MEMCHECK`, not `MEMCHECK_PAYLOAD`.
#[derive(ktstr::Payload)]
#[payload(binary = "memcheck-bin", name = "memcheck")]
#[allow(dead_code)]
struct MemcheckPayload;

#[test]
fn derive_payload_suffix_strip_happens_before_uppercase() {
    assert_eq!(MEMCHECK.name, "memcheck");
    assert!(matches!(MEMCHECK.kind, PayloadKind::Binary("memcheck-bin"),));
}

/// Metric with only `name` set (no polarity, no unit) defaults to
/// `Polarity::Unknown` and empty unit — matches the frozen design's
/// "unhinted metrics" contract.
#[derive(ktstr::Payload)]
#[payload(binary = "bare_metric")]
#[metric(name = "throughput")]
#[allow(dead_code)]
struct BareMetricPayload;

#[test]
fn derive_payload_metric_minimal_defaults_unknown_polarity() {
    assert_eq!(BARE_METRIC.metrics.len(), 1);
    assert_eq!(BARE_METRIC.metrics[0].name, "throughput");
    assert_eq!(BARE_METRIC.metrics[0].polarity, Polarity::Unknown);
    assert_eq!(BARE_METRIC.metrics[0].unit, "");
}

/// Default check uses `range(...)` — checks the macro's bare
/// MetricCheck-constructor resolution against every const-fn constructor,
/// not just the ones used above.
#[derive(ktstr::Payload)]
#[payload(binary = "range_check_bin")]
#[default_check(range("cpu_pct", 10.0, 90.0))]
#[default_check(max("latency_us", 500.0))]
#[default_check(exists("sampling_key"))]
#[allow(dead_code)]
struct RangeCheckPayload;

#[test]
fn derive_payload_default_checks_resolve_all_constructors() {
    assert_eq!(RANGE_CHECK.default_checks.len(), 3);
    assert!(matches!(
        RANGE_CHECK.default_checks[0],
        MetricCheck::Range { metric, lo, hi } if metric == "cpu_pct" && lo == 10.0 && hi == 90.0,
    ));
    assert!(matches!(
        RANGE_CHECK.default_checks[1],
        MetricCheck::Max { metric, value } if metric == "latency_us" && value == 500.0,
    ));
    assert!(matches!(
        RANGE_CHECK.default_checks[2],
        MetricCheck::Exists("sampling_key"),
    ));
}

/// Both bare (`min(...)`) and qualified (`MetricCheck::min(...)`)
/// constructor forms must resolve to the same generated `MetricCheck::...`
/// variant. The macro detects the explicit `MetricCheck` segment on the
/// callee path and skips its implicit `::ktstr::test_support::MetricCheck::`
/// prepend so a user who imports `MetricCheck` and writes the prefix
/// themselves gets `MetricCheck::min(...)`, not `MetricCheck::MetricCheck::min(...)`.
#[derive(ktstr::Payload)]
#[payload(binary = "qualified_check_bin")]
#[default_check(MetricCheck::min("iops", 1000.0))]
#[default_check(MetricCheck::max("latency_us", 500.0))]
#[default_check(exists("sampling_key"))]
#[allow(dead_code)]
struct QualifiedCheckPayload;

#[test]
fn derive_payload_accepts_qualified_check_prefix() {
    assert_eq!(QUALIFIED_CHECK.default_checks.len(), 3);
    assert!(matches!(
        QUALIFIED_CHECK.default_checks[0],
        MetricCheck::Min { metric, value } if metric == "iops" && value == 1000.0,
    ));
    assert!(matches!(
        QUALIFIED_CHECK.default_checks[1],
        MetricCheck::Max { metric, value } if metric == "latency_us" && value == 500.0,
    ));
    assert!(matches!(
        QUALIFIED_CHECK.default_checks[2],
        MetricCheck::Exists("sampling_key"),
    ));
}

/// Explicit `output = ExitCode` must parse through the same
/// PascalCase output grammar as `Json` and emit a
/// Payload whose `.output == OutputFormat::ExitCode`. The default
/// (no `output =` kwarg) also lands at `ExitCode`, but a future
/// change that silently promoted an absent `output` to
/// `OutputFormat::Json` would go undetected without this test —
/// explicit + default both must resolve to `ExitCode`.
#[derive(ktstr::Payload)]
#[payload(binary = "exit_code_bin", output = ExitCode)]
#[allow(dead_code)]
struct ExplicitExitCodePayload;

#[test]
fn derive_payload_explicit_exit_code_output() {
    assert!(matches!(EXPLICIT_EXIT_CODE.output, OutputFormat::ExitCode));
    assert_eq!(EXPLICIT_EXIT_CODE.name, "exit_code_bin");
    assert!(matches!(
        EXPLICIT_EXIT_CODE.kind,
        PayloadKind::Binary("exit_code_bin"),
    ));
}

/// The fully-qualified crate path
/// `::ktstr::test_support::MetricCheck::min(...)` must also resolve
/// through the same prefix-detection branch as the shorter
/// `MetricCheck::min(...)` form. `expr_has_check_prefix` scans every
/// segment for an ident named `MetricCheck`, so any absolute path that
/// carries the type still lands on the user-written callee
/// without the macro double-prepending its implicit
/// `::ktstr::test_support::MetricCheck::` segment.
#[derive(ktstr::Payload)]
#[payload(binary = "fully_qualified_check_bin")]
#[default_check(::ktstr::test_support::MetricCheck::min("iops", 500.0))]
#[default_check(::ktstr::test_support::MetricCheck::exit_code_eq(0))]
#[allow(dead_code)]
struct FullyQualifiedCheckPayload;

#[test]
fn derive_payload_accepts_fully_qualified_check_path() {
    assert_eq!(FULLY_QUALIFIED_CHECK.default_checks.len(), 2);
    assert!(matches!(
        FULLY_QUALIFIED_CHECK.default_checks[0],
        MetricCheck::Min { metric, value } if metric == "iops" && value == 500.0,
    ));
    assert!(matches!(
        FULLY_QUALIFIED_CHECK.default_checks[1],
        MetricCheck::ExitCodeEq(0),
    ));
}

/// `#[include_files(...)]` registers the variadic string literals into
/// the emitted const's `include_files` slice, in declaration order.
/// Regression: the derive body parsed `include_files` but the
/// attribute was not in the derive's `attributes(...)` list, so
/// `#[include_files(...)]` on a `#[derive(Payload)]` struct failed to
/// compile ("cannot find attribute include_files in this scope") —
/// this test only compiles because the helper attribute is registered.
#[derive(ktstr::Payload)]
#[payload(binary = "bench_with_includes")]
#[include_files("bench-helper", "config.json")]
#[allow(dead_code)]
struct BenchWithIncludesPayload;

#[test]
fn derive_payload_include_files_registered_in_order() {
    // The derive auto-injects the `binary` spec as the first
    // include_files entry (payload.rs `include_files.insert(0, binary)`),
    // so `#[payload(binary = "X")]` packages X without a separate
    // `#[include_files("X")]`. The user-declared helpers follow in
    // source order.
    assert_eq!(
        BENCH_WITH_INCLUDES.include_files,
        &["bench_with_includes", "bench-helper", "config.json"],
    );
}
