use Test2::V0;

# Manifest-driven SSR defaults (issue #1416).
#
# `bf build` embeds an `ssrDefaults` map in every component's manifest
# entry. The Mojolicious plugin reads that map at plugin-register
# time and:
#   1. In a `before_render` hook for top-level components, seeds each
#      template variable into the stash so Perl strict mode doesn't
#      reject `$variant` / `$count` / etc.
#   2. In `register_components_from_manifest`, derives per-child
#      `signal_init`-style seed kvs (caller's props win over the
#      static defaults).
#
# These cover the wiring that used to live in every user's `app.pl`.

use FindBin qw($Bin);
use lib "$Bin/../lib";

use Mojo::File qw(tempdir);
use Mojo::JSON qw(encode_json true false);
use Mojolicious;
use Mojolicious::Plugin::BarefootJS;

use BarefootJS;

subtest '_derive_stash_from_defaults — UI registry child shape' => sub {
    # Manifest entry for a Badge-like component: every template
    # variable carries a `propName` mapping to its source prop, plus a
    # rest-props bag entry. The caller's `$props->{...}` value takes
    # precedence over the static default when supplied.
    my $defaults = {
        variant   => { propName => 'variant',   value => 'default' },
        asChild   => { propName => 'asChild',   value => false      },
        className => { propName => 'className', value => ''         },
        props     => { isRestProps => 1,        value => {}         },
    };

    my %merged = BarefootJS::_derive_stash_from_defaults(
        $defaults,
        { variant => 'destructive', className => 'extra-class' },
    );

    is $merged{variant},   'destructive', 'caller-supplied prop overrides static default';
    is $merged{className}, 'extra-class', 'caller-supplied prop overrides empty string default';
    is $merged{asChild},   false,         'unsupplied prop falls back to static default';
    is ref($merged{props}), 'HASH',        'rest-props bag falls back to empty hash';
};

subtest '_derive_stash_from_defaults — signal/memo defaults (no propName)' => sub {
    # Counter-like component: signal getters / memo names have no
    # `propName` because they're internal to the component, so the
    # caller cannot override them — the static evaluator's result is
    # used verbatim.
    my $defaults = {
        count   => { value => 99  },
        doubled => { value => 198 },
    };

    my %merged = BarefootJS::_derive_stash_from_defaults(
        $defaults,
        { count => 'ignored-by-merge-rule' },  # caller can't override no-propName entries
    );

    is $merged{count},   99,  'signal getter sources from static default';
    is $merged{doubled}, 198, 'memo also sources from static default';
};

subtest 'plugin before_render hook seeds the stash from manifest' => sub {
    # Spin up a Mojolicious app with the plugin, simulate a render of
    # a top-level component, and assert that the SSR template's
    # variables are stashed via the auto-init hook.
    my $dir = tempdir;
    my $manifest = {
        Counter => {
            markedTemplate => 'templates/Counter.html.ep',
            ssrDefaults    => {
                count   => { value => 42 },
                doubled => { value => 84 },
            },
        },
        # A UI registry child entry — these should NOT trigger the
        # auto-init hook (they're invoked via render_child, not as a
        # top-level render target).
        'ui/badge/index' => {
            markedTemplate => 'templates/ui/badge/index.html.ep',
            ssrDefaults    => {
                variant => { propName => 'variant', value => 'default' },
            },
        },
    };
    my $manifest_file = $dir->child('manifest.json');
    $manifest_file->spew(encode_json($manifest));

    my $app = Mojolicious->new;
    $app->plugin('BarefootJS' => { manifest_path => "$manifest_file" });
    my $c = $app->build_controller;

    # Trigger the before_render hook with a top-level component.
    $app->plugins->emit_hook(before_render => $c, { template => 'Counter' });

    is $c->stash('count'),   42, 'static signal default reached the stash';
    is $c->stash('doubled'), 84, 'static memo default reached the stash';
    ok defined($c->bf->_scope_id) && length($c->bf->_scope_id),
        'scope_id was auto-generated by the hook';
};

subtest 'plugin before_render — caller stash wins over manifest defaults' => sub {
    my $dir = tempdir;
    my $manifest_file = $dir->child('manifest.json');
    $manifest_file->spew(encode_json({
        Counter => {
            markedTemplate => 'templates/Counter.html.ep',
            ssrDefaults    => { count => { value => 0 } },
        },
    }));

    my $app = Mojolicious->new;
    $app->plugin('BarefootJS' => { manifest_path => "$manifest_file" });
    my $c = $app->build_controller;

    # Caller pre-stashes a value before the render kicks in.
    $c->stash(count => 10);
    $app->plugins->emit_hook(before_render => $c, { template => 'Counter' });

    is $c->stash('count'), 10, 'caller-supplied stash value preserved';
};

subtest 'plugin before_render — resolves an aliased destructured prop\'s CALLER-facing key (#2524)' => sub {
    # `{ n: count }` — the manifest entry is keyed by the LOCAL binding
    # (`count`) but carries `propName: 'n'`, the CALLER-facing key. A route
    # handler stashes the caller-facing prop (`$c->stash(n => 7)`, as it
    # would set any other prop) before rendering; the hook must resolve it
    # onto the template's `count` var, not just fall back to the static
    # default. Before this fix, `$d->{value}` was read directly (ignoring
    # `propName`), so a caller-supplied `n` was silently discarded.
    my $dir = tempdir;
    my $manifest_file = $dir->child('manifest.json');
    $manifest_file->spew(encode_json({
        Badge => {
            markedTemplate => 'templates/Badge.html.ep',
            ssrDefaults    => {
                count => { propName => 'n', value => undef },
                text  => { propName => 'text', value => undef },
            },
        },
    }));

    my $app = Mojolicious->new;
    $app->plugin('BarefootJS' => { manifest_path => "$manifest_file" });
    my $c = $app->build_controller;

    $c->stash(n => 7, text => 'hello');
    $app->plugins->emit_hook(before_render => $c, { template => 'Badge' });

    is $c->stash('count'), 7,       'caller-facing `n` resolved onto the local `count` var';
    is $c->stash('text'),  'hello', 'un-aliased prop still resolves';
};

subtest 'plugin before_render — seeds searchParams from the request query (#1922)' => sub {
    my $dir = tempdir;
    my $manifest_file = $dir->child('manifest.json');
    $manifest_file->spew(encode_json({
        Counter => { markedTemplate => 'templates/Counter.html.ep', ssrDefaults => {} },
    }));

    my $app = Mojolicious->new;
    $app->plugin('BarefootJS' => { manifest_path => "$manifest_file" });
    my $c = $app->build_controller;
    $c->req->url->query->parse('sort=price&page=2');

    $app->plugins->emit_hook(before_render => $c, { template => 'Counter' });

    my $sp = $c->stash('searchParams');
    is ref($sp), 'BarefootJS::SearchParams', 'request-scoped searchParams reader stashed';
    is $sp->get('sort'),    'price', 'reader resolves the live request query';
    is $sp->get('missing'), undef,   'absent key → undef (→ author default in template)';
};

subtest 'plugin before_render — caller searchParams wins over the request' => sub {
    my $dir = tempdir;
    my $manifest_file = $dir->child('manifest.json');
    $manifest_file->spew(encode_json({
        Counter => { markedTemplate => 'templates/Counter.html.ep', ssrDefaults => {} },
    }));

    my $app = Mojolicious->new;
    $app->plugin('BarefootJS' => { manifest_path => "$manifest_file" });
    my $c = $app->build_controller;
    $c->req->url->query->parse('sort=price');
    my $custom = BarefootJS->search_params('sort=custom');
    $c->stash(searchParams => $custom);

    $app->plugins->emit_hook(before_render => $c, { template => 'Counter' });

    is $c->stash('searchParams'), $custom, 'caller-supplied searchParams preserved (//=)';
};

subtest 'plugin loads a manifest that appears after startup (#2126)' => sub {
    # The scaffold's dev script starts `bf build --watch` and the web
    # server concurrently, so the app can boot before the first build
    # writes `manifest.json`. The plugin must pick the manifest up on a
    # later render instead of permanently disabling auto-init (which
    # left every top-level template variable unseeded → 500 under
    # strict).
    my $dir = tempdir;
    my $manifest_file = $dir->child('manifest.json');    # not written yet

    my $app = Mojolicious->new;
    $app->plugin('BarefootJS' => { manifest_path => "$manifest_file" });

    my $c1 = $app->build_controller;
    $app->plugins->emit_hook(before_render => $c1, { template => 'Counter' });
    ok !exists $c1->stash->{count}, 'no seeding while the manifest is missing';

    # First `bf build` completes → the very next render must be seeded.
    $manifest_file->spew(encode_json({
        Counter => {
            markedTemplate => 'templates/Counter.html.ep',
            ssrDefaults    => {
                count   => { value => 0 },
                initial => { propName => 'initial', value => undef },
            },
        },
    }));

    my $c2 = $app->build_controller;
    $app->plugins->emit_hook(before_render => $c2, { template => 'Counter' });
    is $c2->stash('count'), 0, 'signal default seeded once the manifest exists';
    ok exists $c2->stash->{initial},
        'null-valued prop entry still declares the template variable';
};

subtest 'plugin re-reads the manifest when the file changes' => sub {
    # `bf build --watch` rewrites the manifest on every rebuild; a new
    # component (`bf add`) or changed defaults must reach renders
    # without a server restart. The cache is keyed on (mtime, size).
    my $dir = tempdir;
    my $manifest_file = $dir->child('manifest.json');
    $manifest_file->spew(encode_json({
        Counter => {
            markedTemplate => 'templates/Counter.html.ep',
            ssrDefaults    => { count => { value => 1 } },
        },
    }));

    my $app = Mojolicious->new;
    $app->plugin('BarefootJS' => { manifest_path => "$manifest_file" });

    my $c1 = $app->build_controller;
    $app->plugins->emit_hook(before_render => $c1, { template => 'Counter' });
    is $c1->stash('count'), 1, 'initial manifest read';

    # Rewrite with a different value; bump mtime past the 1s stat
    # granularity so the (mtime, size) signature is guaranteed to change.
    $manifest_file->spew(encode_json({
        Counter => {
            markedTemplate => 'templates/Counter.html.ep',
            ssrDefaults    => { count => { value => 2 }, extra => { value => 3 } },
        },
    }));
    my $future = time + 2;
    utime $future, $future, "$manifest_file";

    my $c2 = $app->build_controller;
    $app->plugins->emit_hook(before_render => $c2, { template => 'Counter' });
    is $c2->stash('count'), 2, 'rebuilt manifest is picked up without a restart';
};

subtest 'plugin before_render — skips child templates' => sub {
    my $dir = tempdir;
    my $manifest_file = $dir->child('manifest.json');
    $manifest_file->spew(encode_json({
        'ui/badge/index' => {
            markedTemplate => 'templates/ui/badge/index.html.ep',
            ssrDefaults    => { variant => { propName => 'variant', value => 'default' } },
        },
    }));

    my $app = Mojolicious->new;
    $app->plugin('BarefootJS' => { manifest_path => "$manifest_file" });
    my $c = $app->build_controller;

    $app->plugins->emit_hook(before_render => $c, { template => 'ui/badge/index' });

    ok !defined($c->stash('variant')), 'child entries do not auto-seed stash on render';
};

done_testing;
