"""Canary tests for the plugin scaffolder's frontend generation.

`flask plugin new <name>` generates the backend skeleton and, when a frontend
tree is present, a matching Vue frontend starting point: list, detail, and form
views, a router route file, and a paste-in api-client snippet. These tests
guard the file names, the substitutions, the ADR-009 plugin gating on the
routes, the use of global CSS classes over per-page input styling, graceful
skipping when there is no frontend tree, and the no-clobber behaviour.

All generation runs against tmp_path. The real frontend/src tree is never
touched.
"""

from pathlib import Path

import pytest

from shopdb.plugins.scaffolder import scaffold_plugin, ScaffoldError


def _make_dirs(tmp_path: Path):
    """Create a tmp plugins dir and a tmp frontend/src tree.

    Returns (plugins_dir, frontend_dir).
    """
    plugins_dir = tmp_path / 'plugins'
    plugins_dir.mkdir()
    frontend_dir = tmp_path / 'frontend' / 'src'
    frontend_dir.mkdir(parents=True)
    return plugins_dir, frontend_dir


def test_frontend_files_generated_with_correct_names(tmp_path):
    """Views, route file, and api snippet appear with the expected names."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)

    target = scaffold_plugin(
        name='widgets',
        description='Test widgets plugin',
        plugins_dir=plugins_dir,
        frontend_dir=frontend_dir,
    )

    assert (frontend_dir / 'views' / 'widgets' / 'WidgetsList.vue').exists()
    assert (frontend_dir / 'views' / 'widgets' / 'WidgetsDetail.vue').exists()
    assert (frontend_dir / 'views' / 'widgets' / 'WidgetsForm.vue').exists()
    assert (frontend_dir / 'router' / 'routes' / 'widgets.js').exists()
    # paste-in snippet lands in the plugin dir, not the frontend tree
    assert (target / 'frontend-api-snippet.js').exists()


def test_frontend_substitutions_applied(tmp_path):
    """Name and Name placeholders are substituted, none left raw."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)
    scaffold_plugin('widgets', 'Test', plugins_dir, frontend_dir=frontend_dir)

    route = (frontend_dir / 'router' / 'routes' / 'widgets.js').read_text()
    assert "import('../../views/widgets/WidgetsList.vue')" in route
    assert "import('../../views/widgets/WidgetsForm.vue')" in route
    assert "import('../../views/widgets/WidgetsDetail.vue')" in route

    list_view = (frontend_dir / 'views' / 'widgets' / 'WidgetsList.vue').read_text()
    assert 'widgetsApi' in list_view
    assert "api.get('/widgets'" in list_view
    # no unresolved template placeholders for our known substitution keys
    assert '${name}' not in list_view
    assert '${Name}' not in list_view


def test_route_file_has_plugin_gating_and_requiresauth(tmp_path):
    """Every route carries meta.plugin; form routes add requiresAuth."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)
    scaffold_plugin('widgets', 'Test', plugins_dir, frontend_dir=frontend_dir)

    route = (frontend_dir / 'router' / 'routes' / 'widgets.js').read_text()

    # ADR-009 gating on every route
    assert route.count("plugin: 'widgets'") == 4
    # form routes (new + edit) require auth; list + detail do not
    assert route.count('requiresAuth: true') == 2
    assert "path: 'widgets/new'" in route
    assert "path: 'widgets/:id/edit'" in route


def test_views_use_global_classes_not_custom_input_css(tmp_path):
    """Views lean on global .filters / .form-control / .card classes and never
    redefine input styling in a scoped block."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)
    scaffold_plugin('widgets', 'Test', plugins_dir, frontend_dir=frontend_dir)

    views = frontend_dir / 'views' / 'widgets'
    list_view = (views / 'WidgetsList.vue').read_text()
    form_view = (views / 'WidgetsForm.vue').read_text()

    assert 'class="filters"' in list_view
    assert 'class="form-control"' in list_view
    assert 'class="card"' in list_view
    assert 'class="form-control"' in form_view

    # per-page input styling is forbidden: no scoped rule targets .form-control
    # on its own (a descendant layout helper like `.filters .form-control` is ok)
    import re
    for view in (list_view, form_view):
        assert not re.search(r'(?m)^\s*\.form-control\s*\{', view)


def test_default_frontend_dir_resolves_relative_to_plugins(tmp_path):
    """With no frontend_dir arg, views land in <plugins_dir>/../frontend/src."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)

    scaffold_plugin('widgets', 'Test', plugins_dir)

    assert (frontend_dir / 'views' / 'widgets' / 'WidgetsList.vue').exists()
    assert (frontend_dir / 'router' / 'routes' / 'widgets.js').exists()


def test_skip_when_no_frontend_dir(tmp_path):
    """No frontend tree: views and route are skipped, backend still generated.

    The api snippet is still emitted because it is a plugin-directory artifact.
    """
    plugins_dir = tmp_path / 'plugins'
    plugins_dir.mkdir()
    missing_frontend = tmp_path / 'nowhere' / 'src'

    target = scaffold_plugin(
        name='widgets',
        description='Test',
        plugins_dir=plugins_dir,
        frontend_dir=missing_frontend,
    )

    # backend skeleton is intact
    assert (target / 'manifest.json').exists()
    assert (target / 'plugin.py').exists()
    # frontend views and route were skipped
    assert not missing_frontend.exists()
    # snippet still lands in the plugin dir
    assert (target / 'frontend-api-snippet.js').exists()


def test_frontend_can_be_disabled(tmp_path):
    """frontend=False generates neither views, route, nor snippet."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)

    target = scaffold_plugin(
        name='widgets',
        description='Test',
        plugins_dir=plugins_dir,
        frontend_dir=frontend_dir,
        frontend=False,
    )

    assert not (frontend_dir / 'views' / 'widgets').exists()
    assert not (frontend_dir / 'router' / 'routes' / 'widgets.js').exists()
    assert not (target / 'frontend-api-snippet.js').exists()


def test_no_clobber_without_overwrite(tmp_path):
    """An existing frontend view is left untouched unless overwrite is set."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)

    # author already hand-edited a view before re-scaffolding
    views_dir = frontend_dir / 'views' / 'widgets'
    views_dir.mkdir(parents=True)
    existing = views_dir / 'WidgetsList.vue'
    existing.write_text('SENTINEL do not clobber')

    scaffold_plugin('widgets', 'Test', plugins_dir, frontend_dir=frontend_dir)

    # untouched without overwrite
    assert existing.read_text() == 'SENTINEL do not clobber'


def test_overwrite_replaces_existing_frontend(tmp_path):
    """overwrite=True regenerates an existing frontend view."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)

    views_dir = frontend_dir / 'views' / 'widgets'
    views_dir.mkdir(parents=True)
    existing = views_dir / 'WidgetsList.vue'
    existing.write_text('SENTINEL do not clobber')

    # plugin dir must also exist to reach overwrite path
    scaffold_plugin('widgets', 'Test', plugins_dir, frontend_dir=frontend_dir)
    scaffold_plugin(
        'widgets', 'Test', plugins_dir,
        frontend_dir=frontend_dir, overwrite=True,
    )

    assert existing.read_text() != 'SENTINEL do not clobber'
    assert 'widgetsApi' in existing.read_text()


def test_route_references_only_existing_views(tmp_path):
    """Every view lazy-imported by the route file exists on disk.

    Guards the write-views-before-route ordering: a route pointing at a missing
    view crashes the Vite dev server.
    """
    plugins_dir, frontend_dir = _make_dirs(tmp_path)
    scaffold_plugin('widgets', 'Test', plugins_dir, frontend_dir=frontend_dir)

    route = (frontend_dir / 'router' / 'routes' / 'widgets.js').read_text()
    import re
    imported = re.findall(r"import\('(\.\./\.\./views/[^']+)'\)", route)
    assert imported
    for rel in imported:
        # rel is relative to router/routes/; resolve against that dir
        resolved = (frontend_dir / 'router' / 'routes' / rel).resolve()
        assert resolved.exists(), f'route imports missing view: {rel}'


def test_generated_frontend_is_ascii(tmp_path):
    """Generated frontend files contain plain ASCII only."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)
    target = scaffold_plugin(
        'widgets', 'Test', plugins_dir, frontend_dir=frontend_dir,
    )

    generated = [
        frontend_dir / 'views' / 'widgets' / 'WidgetsList.vue',
        frontend_dir / 'views' / 'widgets' / 'WidgetsDetail.vue',
        frontend_dir / 'views' / 'widgets' / 'WidgetsForm.vue',
        frontend_dir / 'router' / 'routes' / 'widgets.js',
        target / 'frontend-api-snippet.js',
    ]
    for path in generated:
        text = path.read_text()
        text.encode('ascii')  # raises if any non-ASCII slipped in


def test_frontend_dir_not_created_when_absent(tmp_path):
    """Scaffolder does not fabricate a frontend tree that was not there."""
    plugins_dir = tmp_path / 'plugins'
    plugins_dir.mkdir()

    # no frontend dir exists; default resolves to <tmp>/frontend/src (absent)
    scaffold_plugin('widgets', 'Test', plugins_dir)

    assert not (tmp_path / 'frontend').exists()


def test_scaffold_still_raises_on_existing_plugin(tmp_path):
    """Frontend generation does not weaken the plugin-dir overwrite guard."""
    plugins_dir, frontend_dir = _make_dirs(tmp_path)
    scaffold_plugin('widgets', 'first', plugins_dir, frontend_dir=frontend_dir)

    with pytest.raises(ScaffoldError, match='already exists'):
        scaffold_plugin(
            'widgets', 'second', plugins_dir, frontend_dir=frontend_dir,
        )
