"""Tests for the fixed-invocation builder — the INV-T7 enforcement surface.

These assert that (a) a valid request produces exactly the proven §4.6 command shape, and
(b) every out-of-bounds / unsafe / mis-fit request is REJECTED before any argv is produced.
Pure functions, no GPU, no subprocess — runnable anywhere.
"""

import pytest

from worker import config
from worker.invocation import InvalidTrainRequest, build_command, published_relative_path


def _req(**over):
    base = {
        "dataset_dir": "/worktank/arbo/train/t123/dataset",
        "base_model_path": "/opt/fluxgym/models/sdxl/base.safetensors",
        "output_dir": "/worktank/arbo/train/t123/out",
        "output_name": "char_t123",
        "trigger": "ohwx",
        "subject_class": "woman",
        "repeats": 10,
        "tier": "balanced",
        "device_index": 0,
        "seed": 42,
    }
    base.update(over)
    return base


def _flag_value(argv, flag):
    return argv[argv.index(flag) + 1]


# ---- happy path ------------------------------------------------------------------------

def test_balanced_on_3090_builds_proven_shape():
    argv, env, params = build_command(_req(tier="balanced", device_index=0))
    assert str(config.ACCELERATE_BIN) == argv[0]
    assert "launch" == argv[1]
    assert str(config.SDXL_TRAIN_SCRIPT) in argv
    # tier table (§4.6): balanced -> 1500 steps, dim 32, res 768
    assert _flag_value(argv, "--max_train_steps") == "1500"
    assert _flag_value(argv, "--network_dim") == "32"
    assert _flag_value(argv, "--resolution") == "768,768"
    # the load-bearing lean flags + caption gotcha
    assert "--network_train_unet_only" in argv
    assert "--gradient_checkpointing" in argv
    assert _flag_value(argv, "--caption_extension") == ".txt"
    assert _flag_value(argv, "--optimizer_type") == "adamw8bit"
    # env: PCI_BUS_ID ordering + the assigned device + allocator
    assert env["CUDA_DEVICE_ORDER"] == "PCI_BUS_ID"
    assert env["CUDA_VISIBLE_DEVICES"] == "0"
    assert env["PYTORCH_CUDA_ALLOC_CONF"] == "expandable_segments:True"
    assert params["steps"] == 1500


def test_quality_on_a6000_ok_1024():
    argv, env, _ = build_command(_req(tier="quality", device_index=1))
    assert _flag_value(argv, "--resolution") == "1024,1024"
    assert _flag_value(argv, "--max_train_steps") == "3000"
    assert env["CUDA_VISIBLE_DEVICES"] == "1"


def test_paths_and_names_flow_through():
    argv, _, _ = build_command(_req(output_name="my_char", trigger="ohwx"))
    assert _flag_value(argv, "--output_name") == "my_char"
    assert _flag_value(argv, "--train_data_dir") == "/worktank/arbo/train/t123/dataset"


# ---- rejections (INV-T7 / §4.6 tier-device fit) ----------------------------------------

def test_quality_on_3090_rejected():
    with pytest.raises(InvalidTrainRequest, match="quality"):
        build_command(_req(tier="quality", device_index=0))


def test_unknown_tier_rejected():
    with pytest.raises(InvalidTrainRequest, match="tier"):
        build_command(_req(tier="ultra"))


def test_bad_device_index_rejected():
    with pytest.raises(InvalidTrainRequest, match="device_index"):
        build_command(_req(device_index=3))


def test_dataset_dir_outside_handoff_root_rejected():
    with pytest.raises(InvalidTrainRequest, match="dataset_dir"):
        build_command(_req(dataset_dir="/etc/passwd"))


def test_path_traversal_rejected():
    with pytest.raises(InvalidTrainRequest, match="dataset_dir"):
        build_command(_req(dataset_dir="/worktank/arbo/train/../../etc/shadow"))


def test_base_model_outside_allowed_roots_rejected():
    with pytest.raises(InvalidTrainRequest, match="base_model_path"):
        build_command(_req(base_model_path="/home/someone/evil.safetensors"))


def test_published_relative_path():
    # Phase 2 publish: derive {train_id} from the handoff output_dir -> ComfyUI-relative loras path
    assert published_relative_path(
        "/worktank/arbo/train/392cf898ac03/output", "sindra_lora"
    ) == "trained/392cf898ac03/sindra_lora.safetensors"


def test_published_relative_path_explicit_train_id_wins():
    # explicit train_id (arbo now sends it) takes precedence over the path-derived fallback
    assert published_relative_path(
        "/worktank/arbo/train/whatever/output", "sindra_lora", train_id="abc123"
    ) == "trained/abc123/sindra_lora.safetensors"


def test_train_id_optional_and_validated():
    # absent -> OK (falls back to derivation); present+safe -> OK; present+unsafe -> 422
    build_command(_req())  # no train_id key
    build_command(_req(train_id="392cf898ac03"))
    with pytest.raises(InvalidTrainRequest, match="train_id"):
        build_command(_req(train_id="../../etc"))


def test_storetank_checkpoint_allowed():
    # the canonical SDXL store (2026-06-13 move) — arbo dispatches base_model_path from here
    argv, _, _ = build_command(
        _req(base_model_path="/storetank/arbo/models/checkpoints/albedobaseXL_v31Large.safetensors")
    )
    assert _flag_value(argv, "--pretrained_model_name_or_path") == \
        "/storetank/arbo/models/checkpoints/albedobaseXL_v31Large.safetensors"


def test_unsafe_output_name_rejected():
    with pytest.raises(InvalidTrainRequest, match="output_name"):
        build_command(_req(output_name="a; rm -rf /"))


def test_unsafe_trigger_rejected():
    with pytest.raises(InvalidTrainRequest, match="trigger"):
        build_command(_req(trigger="$(whoami)"))


def test_repeats_out_of_range_rejected():
    with pytest.raises(InvalidTrainRequest, match="repeats"):
        build_command(_req(repeats=0))
    with pytest.raises(InvalidTrainRequest, match="repeats"):
        build_command(_req(repeats=1000))


def test_relative_dataset_dir_rejected():
    with pytest.raises(InvalidTrainRequest, match="absolute"):
        build_command(_req(dataset_dir="relative/path"))


def test_alpha_scales_with_dim():
    _, _, params = build_command(_req(tier="fast"))
    # fast -> dim 16; alpha = dim * ratio (0.5, matching Sindra) = 8
    argv, _, _ = build_command(_req(tier="fast"))
    assert _flag_value(argv, "--network_dim") == "16"
    assert _flag_value(argv, "--network_alpha") == "8"
    assert params["dim"] == 16
