src/lightspeed@ops@migration_harness.erl

-module(lightspeed@ops@migration_harness).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/ops/migration_harness.gleam").
-export([run_scenario/1, run_matrix/0, scenario_label/1, pass_fail_label/1, signature/1, scenario/1, outcomes/1, failed_scenarios/1, report_signature/1]).
-export_type([scenario/0, scenario_outcome/0, report/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(" Deterministic M19 harness for scoped data access and migration plans.\n").

-type scenario() :: small_reference_migration |
    medium_reference_migration |
    authz_scoped_access |
    rollback_mixed_runtime.

-type scenario_outcome() :: {scenario_outcome, scenario(), boolean(), binary()}.

-type report() :: {report, list(scenario_outcome()), integer()}.

-file("src/lightspeed/ops/migration_harness.gleam", 257).
-spec count_failed(list(scenario_outcome())) -> integer().
count_failed(Outcomes) ->
    case Outcomes of
        [] ->
            0;

        [Outcome | Rest] ->
            case erlang:element(3, Outcome) of
                true ->
                    count_failed(Rest);

                false ->
                    1 + count_failed(Rest)
            end
    end.

-file("src/lightspeed/ops/migration_harness.gleam", 268).
-spec join_with(binary(), list(binary())) -> binary().
join_with(Separator, Values) ->
    case Values of
        [] ->
            <<""/utf8>>;

        [Value] ->
            Value;

        [Value@1 | Rest] ->
            <<<<Value@1/binary, Separator/binary>>/binary,
                (join_with(Separator, Rest))/binary>>
    end.

-file("src/lightspeed/ops/migration_harness.gleam", 243).
-spec contains_step(
    list(lightspeed@data@migration:step()),
    lightspeed@data@migration:step()
) -> boolean().
contains_step(Steps, Expected) ->
    case Steps of
        [] ->
            false;

        [Step | Rest] ->
            case Step =:= Expected of
                true ->
                    true;

                false ->
                    contains_step(Rest, Expected)
            end
    end.

-file("src/lightspeed/ops/migration_harness.gleam", 219).
-spec evaluate_rollback_mixed_runtime() -> scenario_outcome().
evaluate_rollback_mixed_runtime() ->
    Plan = lightspeed@data@migration:medium_reference_plan(),
    Rollback = lightspeed@data@migration:rollback_steps(Plan),
    Adapters = lightspeed@data@migration:adapters(Plan),
    Adapter_labels = gleam@list:map(
        Adapters,
        fun lightspeed@data@repository:adapter_label/1
    ),
    Rollback_labels = gleam@list:map(
        Rollback,
        fun lightspeed@data@migration:step_label/1
    ),
    Passed = ((lightspeed@data@migration:mode_label(erlang:element(4, Plan)) =:= <<"mixed_runtime"/utf8>>)
    andalso contains_step(Rollback, rollback_to_legacy))
    andalso (erlang:length(Adapters) >= 2),
    {scenario_outcome,
        rollback_mixed_runtime,
        Passed,
        <<<<<<<<<<"mode="/utf8,
                            (lightspeed@data@migration:mode_label(
                                erlang:element(4, Plan)
                            ))/binary>>/binary,
                        "|adapters="/utf8>>/binary,
                    (join_with(<<","/utf8>>, Adapter_labels))/binary>>/binary,
                "|rollback="/utf8>>/binary,
            (join_with(<<","/utf8>>, Rollback_labels))/binary>>}.

-file("src/lightspeed/ops/migration_harness.gleam", 123).
-spec evaluate_authz_scoped_access() -> scenario_outcome().
evaluate_authz_scoped_access() ->
    Repo = lightspeed@data@repository:seed(
        {elixir_bridge, <<"MyApp.Repo"/utf8>>},
        [{record,
                <<"r-1"/utf8>>,
                <<"t-1"/utf8>>,
                <<"u-1"/utf8>>,
                <<"invoice"/utf8>>,
                <<"100"/utf8>>},
            {record,
                <<"r-2"/utf8>>,
                <<"t-2"/utf8>>,
                <<"u-2"/utf8>>,
                <<"invoice"/utf8>>,
                <<"200"/utf8>>}]
    ),
    Viewer = lightspeed@data@repository:tenant_scope(
        <<"actor-viewer"/utf8>>,
        <<"t-1"/utf8>>,
        viewer
    ),
    Editor = lightspeed@data@repository:tenant_scope(
        <<"actor-editor"/utf8>>,
        <<"t-1"/utf8>>,
        editor
    ),
    System = lightspeed@data@repository:system_scope(<<"actor-system"/utf8>>),
    Cross_tenant_read = lightspeed@data@repository:fetch_by_id(
        Repo,
        Viewer,
        <<"r-2"/utf8>>
    ),
    Own_tenant_read = lightspeed@data@repository:fetch_by_id(
        Repo,
        Viewer,
        <<"r-1"/utf8>>
    ),
    Viewer_write_denied = lightspeed@data@repository:upsert(
        Repo,
        Viewer,
        {record,
            <<"r-3"/utf8>>,
            <<"t-1"/utf8>>,
            <<"u-1"/utf8>>,
            <<"invoice"/utf8>>,
            <<"300"/utf8>>}
    ),
    Editor_write_allowed = lightspeed@data@repository:upsert(
        Repo,
        Editor,
        {record,
            <<"r-4"/utf8>>,
            <<"t-1"/utf8>>,
            <<"u-1"/utf8>>,
            <<"invoice"/utf8>>,
            <<"400"/utf8>>}
    ),
    System_cross_tenant_delete = lightspeed@data@repository:delete_by_id(
        Repo,
        System,
        <<"r-2"/utf8>>
    ),
    Cross_tenant_denied = case Cross_tenant_read of
        {error, Error} ->
            lightspeed@data@repository:error_label(Error);

        {ok, _} ->
            <<"unexpected_ok"/utf8>>
    end,
    Own_tenant_result = case Own_tenant_read of
        {ok, Record} ->
            <<"ok:"/utf8, (erlang:element(2, Record))/binary>>;

        {error, Error@1} ->
            lightspeed@data@repository:error_label(Error@1)
    end,
    Viewer_write_result = case Viewer_write_denied of
        {error, Error@2} ->
            lightspeed@data@repository:error_label(Error@2);

        {ok, _} ->
            <<"unexpected_ok"/utf8>>
    end,
    Editor_write_result = case Editor_write_allowed of
        {ok, Updated} ->
            <<"ok:"/utf8,
                (erlang:integer_to_binary(
                    erlang:length(lightspeed@data@repository:all(Updated))
                ))/binary>>;

        {error, Error@3} ->
            lightspeed@data@repository:error_label(Error@3)
    end,
    System_delete_result = case System_cross_tenant_delete of
        {ok, Updated@1} ->
            <<"ok:"/utf8,
                (erlang:integer_to_binary(
                    erlang:length(lightspeed@data@repository:all(Updated@1))
                ))/binary>>;

        {error, Error@4} ->
            lightspeed@data@repository:error_label(Error@4)
    end,
    Passed = ((((Cross_tenant_denied =:= <<"forbidden:actor-viewer:read:t-2"/utf8>>)
    andalso (Own_tenant_result =:= <<"ok:r-1"/utf8>>))
    andalso (Viewer_write_result =:= <<"forbidden:actor-viewer:write:t-1"/utf8>>))
    andalso (Editor_write_result =:= <<"ok:3"/utf8>>))
    andalso (System_delete_result =:= <<"ok:1"/utf8>>),
    {scenario_outcome,
        authz_scoped_access,
        Passed,
        <<<<<<<<<<<<<<<<<<<<<<"adapter="/utf8,
                                                    (lightspeed@data@repository:adapter_label(
                                                        lightspeed@data@repository:adapter(
                                                            Repo
                                                        )
                                                    ))/binary>>/binary,
                                                "|cross_tenant="/utf8>>/binary,
                                            Cross_tenant_denied/binary>>/binary,
                                        "|own_tenant="/utf8>>/binary,
                                    Own_tenant_result/binary>>/binary,
                                "|viewer_write="/utf8>>/binary,
                            Viewer_write_result/binary>>/binary,
                        "|editor_write="/utf8>>/binary,
                    Editor_write_result/binary>>/binary,
                "|system_delete="/utf8>>/binary,
            System_delete_result/binary>>}.

-file("src/lightspeed/ops/migration_harness.gleam", 112).
-spec evaluate_medium_reference_migration() -> scenario_outcome().
evaluate_medium_reference_migration() ->
    Plan = lightspeed@data@migration:medium_reference_plan(),
    Valid = lightspeed@data@migration:valid(Plan),
    {scenario_outcome,
        medium_reference_migration,
        Valid,
        lightspeed@data@migration:signature(Plan)}.

-file("src/lightspeed/ops/migration_harness.gleam", 101).
-spec evaluate_small_reference_migration() -> scenario_outcome().
evaluate_small_reference_migration() ->
    Plan = lightspeed@data@migration:small_reference_plan(),
    Valid = lightspeed@data@migration:valid(Plan),
    {scenario_outcome,
        small_reference_migration,
        Valid,
        lightspeed@data@migration:signature(Plan)}.

-file("src/lightspeed/ops/migration_harness.gleam", 40).
?DOC(" Run one scenario.\n").
-spec run_scenario(scenario()) -> scenario_outcome().
run_scenario(Scenario) ->
    case Scenario of
        small_reference_migration ->
            evaluate_small_reference_migration();

        medium_reference_migration ->
            evaluate_medium_reference_migration();

        authz_scoped_access ->
            evaluate_authz_scoped_access();

        rollback_mixed_runtime ->
            evaluate_rollback_mixed_runtime()
    end.

-file("src/lightspeed/ops/migration_harness.gleam", 27).
?DOC(" Run all M19 scenarios.\n").
-spec run_matrix() -> report().
run_matrix() ->
    Scenarios = [small_reference_migration,
        medium_reference_migration,
        authz_scoped_access,
        rollback_mixed_runtime],
    Outcomes = gleam@list:map(Scenarios, fun run_scenario/1),
    {report, Outcomes, count_failed(Outcomes)}.

-file("src/lightspeed/ops/migration_harness.gleam", 50).
?DOC(" Scenario label.\n").
-spec scenario_label(scenario()) -> binary().
scenario_label(Scenario) ->
    case Scenario of
        small_reference_migration ->
            <<"small_reference_migration"/utf8>>;

        medium_reference_migration ->
            <<"medium_reference_migration"/utf8>>;

        authz_scoped_access ->
            <<"authz_scoped_access"/utf8>>;

        rollback_mixed_runtime ->
            <<"rollback_mixed_runtime"/utf8>>
    end.

-file("src/lightspeed/ops/migration_harness.gleam", 60).
?DOC(" Stable pass/fail label.\n").
-spec pass_fail_label(scenario_outcome()) -> binary().
pass_fail_label(Outcome) ->
    case erlang:element(3, Outcome) of
        true ->
            <<"pass"/utf8>>;

        false ->
            <<"fail"/utf8>>
    end.

-file("src/lightspeed/ops/migration_harness.gleam", 68).
?DOC(" Scenario signature.\n").
-spec signature(scenario_outcome()) -> binary().
signature(Outcome) ->
    erlang:element(4, Outcome).

-file("src/lightspeed/ops/migration_harness.gleam", 73).
?DOC(" Scenario accessor.\n").
-spec scenario(scenario_outcome()) -> scenario().
scenario(Outcome) ->
    erlang:element(2, Outcome).

-file("src/lightspeed/ops/migration_harness.gleam", 78).
?DOC(" Report outcomes.\n").
-spec outcomes(report()) -> list(scenario_outcome()).
outcomes(Report) ->
    erlang:element(2, Report).

-file("src/lightspeed/ops/migration_harness.gleam", 83).
?DOC(" Failed scenario count.\n").
-spec failed_scenarios(report()) -> integer().
failed_scenarios(Report) ->
    erlang:element(3, Report).

-file("src/lightspeed/ops/migration_harness.gleam", 88).
?DOC(" Stable report signature for M19 fixture gates.\n").
-spec report_signature(report()) -> binary().
report_signature(Report) ->
    Entries = gleam@list:map(
        erlang:element(2, Report),
        fun(Outcome) ->
            <<<<<<<<(scenario_label(erlang:element(2, Outcome)))/binary,
                            "="/utf8>>/binary,
                        (pass_fail_label(Outcome))/binary>>/binary,
                    ":"/utf8>>/binary,
                (erlang:element(4, Outcome))/binary>>
        end
    ),
    join_with(<<";"/utf8>>, Entries).