# Ícone "Endereços" no Card de Separação (fila única) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** No card de item da missão de Separação (pipeline "fila única"/Luz), exibir um ícone "Endereços" que, ao ser clicado, abre um modal listando todos os endereços WMS onde o produto atual está registrado (saldo local) e a quantidade em cada um.

**Architecture:** Sonda nova agrega `wms_movimentos` por produto (batch, sem N+1). Um Laboratório-fachada em `AlocacaoWms` encapsula essa consulta (mesmo padrão de `ResolverEnderecoProdutoLaboratorio`). O sinal `App` expõe uma rota de modal (`abrirModal()`, mecanismo já usado por `statusSeparacaoCarregamento`/`sugerirCodigoBarrasModal`) que consome o Laboratório e renderiza a lista. Um componente PHP plugável (`enderecosProdutoWmsBadge()`) desenha o ícone dentro da área de badges já existente em `execucao_card.php`.

**Tech Stack:** PHP 8 (Galaxia framework), Postgres (`dev_gx_goldie`), jQuery/Bootstrap-like modal do tema (sem JS novo nesta entrega).

## Global Constraints

- Escopo **só** do pipeline "fila única"/Luz (`App/html/luz/separacao/item_card.php` → `execucaoCard()` sem `modo_dinamico`). O pipeline "modo livre" (`App/html/separacao/item.php`, `modo_dinamico=true`) fica **explicitamente fora de escopo** — a função nova deve ficar inerte (sem desenhar nada) quando `modo_dinamico` é true, para não gerar comportamento incorreto (ícone preso no produto do primeiro render).
- Quantidade exibida = soma local de `wms_movimentos` com `status='confirmada'` (não é o saldo oficial do WMS externo — não há API de leitura do WMS neste repo).
- Produto sem nenhum movimento confirmado → ícone não aparece (sem clique em vazio).
- Sem migrations novas, sem endpoint `/api/v1` novo — só Laboratório interno consumido diretamente pelo sinal `App` via `abrirModal()`.
- Testes de integração (Sonda/Laboratório) rodam via CLI contra `dev_gx_goldie` (nunca prod), seguindo o padrão `*TddTest.php` já usado no sinal `AlocacaoWms` — sem PHPUnit, sem mocks.

---

### Task 1: `WmsMovimentoSonda::mapaSaldoPorProduto()`

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php:252` (logo após o método `saldoPorLocalizacaoProduto()`, que termina na linha 252)
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaMapaSaldoPorProdutoTddTest.php`

**Interfaces:**
- Produces: `WmsMovimentoSonda::mapaSaldoPorProduto(array $produtoIds): array` — retorna `produto_id => [ ['localizacao_codigo' => string, 'quantidade' => float], ... ]`, ordenado por `quantidade` DESC. Produto sem nenhum movimento `status='confirmada'` não aparece no retorno. `mapaSaldoPorProduto([])` retorna `[]`. Usado por `EnderecosProdutoWmsLaboratorio` (Task 2).

- [ ] **Step 1: Escrever o teste (falhando)**

Criar `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaMapaSaldoPorProdutoTddTest.php`:

```php
<?php

/**
 * TddTest — WmsMovimentoSonda::mapaSaldoPorProduto (integration, dev_gx_goldie)
 *
 * Cobre:
 *   - soma quantidade por produto+endereço (2 movimentos no mesmo endereço somam);
 *   - produto com saldo em 2 endereços -> lista ordenada por quantidade DESC;
 *   - produto sem nenhum movimento -> não aparece no mapa;
 *   - mapaSaldoPorProduto([]) -> [];
 *   - filtra apenas os IDs pedidos (não vaza todo o mapa do tenant).
 *
 * Usa produto_id fictício fora da faixa real de catálogo (não há FK produto_id em
 * wms_movimentos) para não colidir com dados reais do tenant. Cleanup no finally.
 */

if (php_sapi_name() !== 'cli') {
    die("Apenas via CLI\n");
}

$_SERVER['HTTP_HOST']      = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';

require_once __DIR__ . '/../../../../../../../autoload.php';

if (session_status() === PHP_SESSION_NONE) {
    @session_start();
}
$info                          = new stdClass();
$info->userData                = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test'];
$info->customerData            = (object)['id' => 22];
$info->principalEmpresa        = (object)['empresa_id' => 3];
$info->sectorData              = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo']              = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use Galaxia\_Controladores\Connect;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') {
    fwrite(STDERR, "ABORT: não conectou em dev_gx_goldie\n");
    exit(1);
}

$passou = 0;
$falhas = 0;
function check(bool $cond, string $msg): void
{
    global $passou, $falhas;
    if ($cond) { $passou++; echo "[PASS] {$msg}\n"; }
    else        { $falhas++; echo "[FAIL] {$msg}\n"; }
}

$BLOCK          = 22;
$PROD_2_ENDER   = 999881001; // saldo em 2 endereços, quantidades diferentes
$PROD_1_ENDER   = 999881002; // saldo em 1 endereço, 2 movimentos somados
$PROD_SEM_SALDO = 999881003; // nunca alocado
$sonda          = new WmsMovimentoSonda();

$MISSAO_ID = (int)$pdo->query("SELECT id FROM missao_logistica WHERE block = {$BLOCK} LIMIT 1")->fetchColumn();
if (!$MISSAO_ID) {
    fwrite(STDERR, "ABORT: sem missão no block {$BLOCK} para satisfazer a FK\n");
    exit(1);
}

echo "====================================================\n";
echo "INTEGRATION — WmsMovimentoSonda::mapaSaldoPorProduto (dev_gx_goldie)\n";
echo "====================================================\n\n";

try {
    $pdo->exec("DELETE FROM wms_movimentos WHERE produto_id IN ({$PROD_2_ENDER}, {$PROD_1_ENDER}, {$PROD_SEM_SALDO})");

    // Produto com saldo em 2 endereços: A01 com 5, B02 com 10 -> B02 vem primeiro (DESC)
    $sonda->registrarAlocacao($MISSAO_ID, null, $PROD_2_ENDER, 'A01-P1-N1', null, 5.0, 'manual', 1);
    $sonda->registrarAlocacao($MISSAO_ID, null, $PROD_2_ENDER, 'B02-P1-N1', null, 10.0, 'manual', 1);

    // Produto com 2 movimentos no MESMO endereço -> soma (3 + 4 = 7)
    $sonda->registrarAlocacao($MISSAO_ID, null, $PROD_1_ENDER, 'C03-P1-N1', null, 3.0, 'manual', 1);
    $sonda->registrarAlocacao($MISSAO_ID, null, $PROD_1_ENDER, 'C03-P1-N1', null, 4.0, 'manual', 1);

    $mapa = $sonda->mapaSaldoPorProduto([$PROD_2_ENDER, $PROD_1_ENDER, $PROD_SEM_SALDO]);

    check(is_array($mapa), 'retorna array');
    check(count($mapa[$PROD_2_ENDER] ?? []) === 2, 'produto com 2 endereços -> 2 entradas na lista');
    check(($mapa[$PROD_2_ENDER][0]['localizacao_codigo'] ?? null) === 'B02-P1-N1', 'lista ordenada por quantidade DESC (B02=10 primeiro)');
    check((float)($mapa[$PROD_2_ENDER][0]['quantidade'] ?? 0) === 10.0, 'quantidade do 1º endereço === 10.0');
    check(count($mapa[$PROD_1_ENDER] ?? []) === 1, 'produto com 2 movimentos no mesmo endereço -> 1 entrada (somada)');
    check((float)($mapa[$PROD_1_ENDER][0]['quantidade'] ?? 0) === 7.0, 'soma de 3.0 + 4.0 === 7.0 no mesmo endereço');
    check(!array_key_exists($PROD_SEM_SALDO, $mapa), 'produto sem nenhum movimento NÃO aparece no mapa');

    echo "\n--- mapaSaldoPorProduto([]) ---\n";
    check($sonda->mapaSaldoPorProduto([]) === [], 'lista de produto_ids vazia -> []');

    echo "\n--- filtra apenas os IDs pedidos ---\n";
    $mapaFiltrado = $sonda->mapaSaldoPorProduto([$PROD_2_ENDER]);
    check(count($mapaFiltrado) === 1 && array_key_exists($PROD_2_ENDER, $mapaFiltrado), 'mapaSaldoPorProduto([X]) só traz X');
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE produto_id IN ({$PROD_2_ENDER}, {$PROD_1_ENDER}, {$PROD_SEM_SALDO})");
}

echo "\n====================================================\n";
echo "RESULTADO: PASS={$passou}  FAIL={$falhas}\n";
if ($falhas > 0) {
    exit(1);
}
echo "TODOS OS TESTES PASSARAM\n";
```

- [ ] **Step 2: Rodar o teste e confirmar que falha**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaMapaSaldoPorProdutoTddTest.php`
Expected: erro fatal `Call to undefined method WmsMovimentoSonda::mapaSaldoPorProduto()`

- [ ] **Step 3: Implementar o método**

Em `componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php`, inserir logo após o fechamento do método `saldoPorLocalizacaoProduto()` (linha 252, antes do método `cancelarPorProdutoLocalizacao` que começa na linha 267):

```php
    /**
     * Mapa produto_id => lista de endereços com saldo local, para batch de N
     * produtos. Cada entrada: [ ['localizacao_codigo' => string, 'quantidade' =>
     * float], ... ], ordenada por quantidade DESC. Mesma semântica de saldo de
     * saldoPorLocalizacaoProduto() — soma local (Genesis), não é o saldo
     * oficial do WMS externo.
     *
     * @param array<int> $produtoIds
     * @return array<int, array<int, array{localizacao_codigo: string, quantidade: float}>>
     */
    public function mapaSaldoPorProduto(array $produtoIds): array
    {
        $ids = array_values(array_unique(array_filter(
            array_map('intval', $produtoIds),
            fn($id) => $id > 0
        )));

        if (empty($ids)) {
            return [];
        }

        $confirmada = StatusAlocacao::CONFIRMADA->value;
        // IDs já são inteiros validados; interpolação segura (PDO não expande arrays).
        $rows = $this->find(
            "produto_id IN (" . implode(',', $ids) . ") AND status = '{$confirmada}'",
            ''
        )->fetch(true);

        if (!$rows) {
            return [];
        }
        if (!is_array($rows)) {
            $rows = [$rows];
        }

        // Agrega por produto_id + localizacao_codigo (soma quantidade).
        $somasPorProdutoLocal = [];
        foreach ($rows as $row) {
            $pid = (int)$row->produto_id;
            $loc = $row->localizacao_codigo;
            $qtd = (float)($row->quantidade ?? 0);
            if (!isset($somasPorProdutoLocal[$pid][$loc])) {
                $somasPorProdutoLocal[$pid][$loc] = 0.0;
            }
            $somasPorProdutoLocal[$pid][$loc] += $qtd;
        }

        $mapa = [];
        foreach ($somasPorProdutoLocal as $pid => $locais) {
            $lista = [];
            foreach ($locais as $codigo => $qtd) {
                $lista[] = ['localizacao_codigo' => $codigo, 'quantidade' => $qtd];
            }
            usort($lista, fn($a, $b) => $b['quantidade'] <=> $a['quantidade']);
            $mapa[$pid] = $lista;
        }

        return $mapa;
    }
```

- [ ] **Step 4: Rodar o teste e confirmar que passa**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaMapaSaldoPorProdutoTddTest.php`
Expected: `TODOS OS TESTES PASSARAM` (PASS=8, FAIL=0)

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaMapaSaldoPorProdutoTddTest.php
git commit -m "feat(alocacaowms): adiciona WmsMovimentoSonda::mapaSaldoPorProduto()"
```

---

### Task 2: `EnderecosProdutoWmsLaboratorio`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/EnderecosProdutoWmsLaboratorio.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/EnderecosProdutoWmsLaboratorioTddTest.php`

**Interfaces:**
- Consumes: `WmsMovimentoSonda::mapaSaldoPorProduto(array $produtoIds): array` (Task 1).
- Produces: `EnderecosProdutoWmsLaboratorio::resolverMapa(array $produtoIds): array` — mesmo formato de retorno de `mapaSaldoPorProduto()`, batch-sanitizado. Usado por `App::enderecosProdutoWms()` (Task 3) e por `enderecosProdutoWmsBadge()` (Task 4).

- [ ] **Step 1: Escrever o teste (falhando)**

Criar `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/EnderecosProdutoWmsLaboratorioTddTest.php`:

```php
<?php

/**
 * TddTest — EnderecosProdutoWmsLaboratorio (integration, dev_gx_goldie)
 *
 * Cobre:
 *   - resolverMapa delega para WmsMovimentoSonda::mapaSaldoPorProduto() e
 *     devolve o mesmo formato (produto_id => lista de endereços);
 *   - produto sem nenhum movimento -> não aparece no mapa;
 *   - resolverMapa([]) -> [];
 *   - sanitiza IDs inválidos (0, negativo, duplicado) antes de consultar.
 *
 * Usa produto_id fictício fora da faixa real de catálogo (não há FK produto_id em
 * wms_movimentos) para não colidir com dados reais do tenant. Cleanup no finally.
 */

if (php_sapi_name() !== 'cli') {
    die("Apenas via CLI\n");
}

$_SERVER['HTTP_HOST']      = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';

require_once __DIR__ . '/../../../../../../../autoload.php';

if (session_status() === PHP_SESSION_NONE) {
    @session_start();
}
$info                          = new stdClass();
$info->userData                = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test'];
$info->customerData            = (object)['id' => 22];
$info->principalEmpresa        = (object)['empresa_id' => 3];
$info->sectorData              = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo']              = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use Galaxia\_Controladores\Connect;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios\EnderecosProdutoWmsLaboratorio;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') {
    fwrite(STDERR, "ABORT: não conectou em dev_gx_goldie\n");
    exit(1);
}

$passou = 0;
$falhas = 0;
function check(bool $cond, string $msg): void
{
    global $passou, $falhas;
    if ($cond) { $passou++; echo "[PASS] {$msg}\n"; }
    else        { $falhas++; echo "[FAIL] {$msg}\n"; }
}

$BLOCK        = 22;
$PROD_COM     = 999882001; // com movimento confirmado
$PROD_SEM     = 999882002; // nunca alocado
$sonda        = new WmsMovimentoSonda();

$MISSAO_ID = (int)$pdo->query("SELECT id FROM missao_logistica WHERE block = {$BLOCK} LIMIT 1")->fetchColumn();
if (!$MISSAO_ID) {
    fwrite(STDERR, "ABORT: sem missão no block {$BLOCK} para satisfazer a FK\n");
    exit(1);
}

echo "====================================================\n";
echo "INTEGRATION — EnderecosProdutoWmsLaboratorio (dev_gx_goldie)\n";
echo "====================================================\n\n";

try {
    $pdo->exec("DELETE FROM wms_movimentos WHERE produto_id IN ({$PROD_COM}, {$PROD_SEM})");

    $sonda->registrarAlocacao($MISSAO_ID, null, $PROD_COM, 'D04-P1-N1', null, 12.0, 'manual', 1);

    $lab = new EnderecosProdutoWmsLaboratorio();

    echo "--- Mapa batch (com + sem) ---\n";
    $mapa = $lab->resolverMapa([$PROD_COM, $PROD_SEM]);

    check(count($mapa[$PROD_COM] ?? []) === 1, 'produto com movimento -> 1 endereço no mapa');
    check(($mapa[$PROD_COM][0]['localizacao_codigo'] ?? null) === 'D04-P1-N1', 'endereço retornado é o esperado');
    check((float)($mapa[$PROD_COM][0]['quantidade'] ?? 0) === 12.0, 'quantidade retornada é a esperada');
    check(!array_key_exists($PROD_SEM, $mapa), 'produto sem movimento NÃO aparece no mapa');

    echo "\n--- resolverMapa([]) ---\n";
    check($lab->resolverMapa([]) === [], 'lista de produto_ids vazia -> []');

    echo "\n--- sanitiza IDs inválidos (0, negativo, duplicado) ---\n";
    $mapaSanitizado = $lab->resolverMapa([$PROD_COM, $PROD_COM, 0, -5]);
    check(count($mapaSanitizado) === 1 && array_key_exists($PROD_COM, $mapaSanitizado), 'IDs inválidos/duplicados são ignorados, produto válido continua presente');
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE produto_id IN ({$PROD_COM}, {$PROD_SEM})");
}

echo "\n====================================================\n";
echo "RESULTADO: PASS={$passou}  FAIL={$falhas}\n";
if ($falhas > 0) {
    exit(1);
}
echo "TODOS OS TESTES PASSARAM\n";
```

- [ ] **Step 2: Rodar o teste e confirmar que falha**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/EnderecosProdutoWmsLaboratorioTddTest.php`
Expected: erro fatal `Class "GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios\EnderecosProdutoWmsLaboratorio" not found`

- [ ] **Step 3: Implementar o Laboratório**

Criar `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/EnderecosProdutoWmsLaboratorio.php`:

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios;

use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda;

/**
 * EnderecosProdutoWmsLaboratorio — batch produto_id -> lista de endereços WMS
 * com saldo local, para o ícone "Endereços" no card de item da Separação.
 *
 * Fica no Sinal AlocacaoWms (dono das Sondas WMS) para manter o conhecimento
 * WMS encapsulado e reutilizável — o Sinal App apenas consome, mesmo padrão
 * de ResolverEnderecoProdutoLaboratorio.
 */
class EnderecosProdutoWmsLaboratorio
{
    private WmsMovimentoSonda $movimentoSonda;

    public function __construct()
    {
        $this->movimentoSonda = new WmsMovimentoSonda();
    }

    /**
     * Resolve os endereços com saldo local para cada produto_id informado.
     * Uma única query batchada (via Sonda) cobre TODOS os produtos pedidos —
     * nada de N+1 por item.
     *
     * @param array<int> $produtoIds
     * @return array<int, array<int, array{localizacao_codigo: string, quantidade: float}>>
     *   produto_id => lista de endereços (ordenada por quantidade DESC). Produtos
     *   sem nenhum movimento confirmado NÃO aparecem no retorno.
     */
    public function resolverMapa(array $produtoIds): array
    {
        $ids = array_values(array_unique(array_filter(
            array_map('intval', $produtoIds),
            fn($id) => $id > 0
        )));

        if (empty($ids)) {
            return [];
        }

        return $this->movimentoSonda->mapaSaldoPorProduto($ids);
    }
}
```

- [ ] **Step 4: Rodar o teste e confirmar que passa**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/EnderecosProdutoWmsLaboratorioTddTest.php`
Expected: `TODOS OS TESTES PASSARAM` (PASS=6, FAIL=0)

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/EnderecosProdutoWmsLaboratorio.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/EnderecosProdutoWmsLaboratorioTddTest.php
git commit -m "feat(alocacaowms): adiciona EnderecosProdutoWmsLaboratorio (fachada produto->endereços+quantidade)"
```

---

### Task 3: Rota de modal em `App.php` + view

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/App/App.php:201-203` (novo bloco de dispatch, logo após o de `sugerirCodigoBarrasModal`), e após o método `sugerirCodigoBarrasModal()` (que termina na linha 466) para adicionar o novo método
- Create: `componentes/Missoes/Logistica/sinais/App/html/shared/wms/modal_enderecos_produto.php`
- Test: `componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsModalTddTest.php`

**Interfaces:**
- Consumes: `EnderecosProdutoWmsLaboratorio::resolverMapa(array $produtoIds): array` (Task 2).
- Produces: rota `abrirModal('enderecosProdutoWms?produto_id=X&produto_nome=Y', ['startSinal'=>'App'])` — consumida pelo botão criado na Task 4.

Esta etapa não tem acesso fácil a um servidor rodando para um teste de integração ponta-a-ponta; o teste aqui é de **inspeção de código-fonte** (mesmo padrão de `App/Testes/Html/CubagemCarregamentoUiTddTest.php`), verificando que o dispatch e a view existem com os contratos certos.

- [ ] **Step 1: Escrever o teste (falhando)**

Criar `componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsModalTddTest.php`:

```php
<?php

/**
 * TDD — Rota de modal enderecosProdutoWms em App.php + view do modal.
 *
 * Testes de inspeção de fonte (mesmo padrão de CubagemCarregamentoUiTddTest):
 * 1. App::start() despacha 'enderecosProdutoWms' para o método correspondente
 * 2. App::enderecosProdutoWms() lê produto_id, chama o Laboratório e renderiza a view
 * 3. A view do modal existe e usa showModalHeader()
 */

class AssertEnderecosWmsModal
{
    public static int $passou = 0;
    public static int $falhas = 0;

    public static function verdadeiro(bool $condicao, string $msg): void
    {
        if ($condicao) {
            self::$passou++;
            echo "✅ [PASS] $msg\n";
        } else {
            self::$falhas++;
            echo "❌ [FAIL] $msg\n";
        }
    }

    public static function contemTexto(string $src, string $trecho, string $msg): void
    {
        self::verdadeiro(strpos($src, $trecho) !== false, $msg);
    }

    public static function summary(): void
    {
        echo "\n--- RESUMO ---\n";
        echo "Passou: " . self::$passou . " | Falhou: " . self::$falhas . "\n";
        if (self::$falhas > 0) exit(1);
        echo "Tests passed\n";
    }
}

$appPhp    = '/home/goldie/www/genesis/componentes/Missoes/Logistica/sinais/App/App.php';
$modalView = '/home/goldie/www/genesis/componentes/Missoes/Logistica/sinais/App/html/shared/wms/modal_enderecos_produto.php';

$srcApp = file_get_contents($appPhp);

echo "Iniciando Testes: EnderecosProdutoWmsModalTddTest\n\n";
echo "── App.php — dispatch ────────────────────────────────────────\n";

AssertEnderecosWmsModal::contemTexto($srcApp, "strpos(\$sinal, 'enderecosProdutoWms')",
    'App::start() verifica sinalGxData contra enderecosProdutoWms');

AssertEnderecosWmsModal::contemTexto($srcApp, 'function enderecosProdutoWms(',
    'App.php declara o método enderecosProdutoWms()');

AssertEnderecosWmsModal::contemTexto($srcApp, 'EnderecosProdutoWmsLaboratorio',
    'enderecosProdutoWms() referencia EnderecosProdutoWmsLaboratorio');

AssertEnderecosWmsModal::contemTexto($srcApp, "html/shared/wms/modal_enderecos_produto",
    'enderecosProdutoWms() renderiza a view modal_enderecos_produto');

echo "\n── View do modal ─────────────────────────────────────────────\n";

AssertEnderecosWmsModal::verdadeiro(file_exists($modalView), 'Arquivo modal_enderecos_produto.php existe');

if (file_exists($modalView)) {
    $srcModal = file_get_contents($modalView);
    AssertEnderecosWmsModal::contemTexto($srcModal, 'showModalHeader(',
        'View do modal usa showModalHeader()');
    AssertEnderecosWmsModal::contemTexto($srcModal, '$enderecos',
        'View do modal itera sobre $enderecos');
}

AssertEnderecosWmsModal::summary();
```

- [ ] **Step 2: Rodar o teste e confirmar que falha**

Run: `php componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsModalTddTest.php`
Expected: falhas nas 5 verificações (dispatch/método/laboratório/view ainda não existem)

- [ ] **Step 3: Implementar o dispatch em `App.php`**

Em `componentes/Missoes/Logistica/sinais/App/App.php`, logo após o bloco (linhas 201-203):
```php
        if (strpos($sinal, 'sugerirCodigoBarrasModal') !== false) {
            return $this->sugerirCodigoBarrasModal($data);
        }
```
adicionar:
```php
        if (strpos($sinal, 'enderecosProdutoWms') !== false) {
            return $this->enderecosProdutoWms($data);
        }
```

- [ ] **Step 4: Implementar o método `enderecosProdutoWms()`**

Logo após o fechamento do método `sugerirCodigoBarrasModal()` (linha 466, antes de `public function ajustedados(array $dados)`), adicionar:

```php
    /**
     * Modal: lista os endereços WMS (saldo local) onde um produto está
     * registrado, com a quantidade em cada um — ícone "Endereços" no card de
     * item da Separação (fila única).
     *
     * Parâmetros (vêm do abrirModal()): produto_id, produto_nome (opc, título).
     */
    public function enderecosProdutoWms(?array $data = [])
    {
        $this->galaxiaRoute->showOnView(false);

        $produtoId   = (int)($data['produto_id'] ?? $_GET['produto_id'] ?? 0);
        $produtoNome = (string)($data['produto_nome'] ?? $_GET['produto_nome'] ?? '');

        $enderecos = [];
        if ($produtoId > 0) {
            $laboratorio = new \GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios\EnderecosProdutoWmsLaboratorio();
            $mapa        = $laboratorio->resolverMapa([$produtoId]);
            $enderecos   = $mapa[$produtoId] ?? [];
        }

        $html = $this->galaxiaRoute->visual('html/shared/wms/modal_enderecos_produto', [
            'produtoNome' => $produtoNome,
            'enderecos'   => $enderecos,
        ], []);

        $this->galaxiaRoute->addResponse($this->routeId, ['html' => $html]);
        $this->response = $this->galaxiaRoute->getResponse($this->routeId);
        return $this;
    }
```

- [ ] **Step 5: Criar a view do modal**

Criar `componentes/Missoes/Logistica/sinais/App/html/shared/wms/modal_enderecos_produto.php`:

```php
<?php
/**
 * Modal: lista de endereços WMS (saldo local) de um produto — ícone
 * "Endereços" no card de item da Separação (fila única).
 *
 * Vars esperadas:
 *   $produtoNome (string, opcional — exibido no título)
 *   $enderecos   (array<int, array{localizacao_codigo: string, quantidade: float}>)
 */

$produtoNome = trim((string) ($produtoNome ?? ''));
$enderecos   = $enderecos ?? [];

echo showModalHeader('Endereços', $produtoNome !== '' ? $produtoNome : 'Produto');
?>
<div class="g-modal-conteudo g-enderecos-wms-body">
    <?php if (empty($enderecos)): ?>
        <p class="g-enderecos-wms-vazio"><?= htmlspecialchars(show('Nenhum endereço registrado para este produto.')) ?></p>
    <?php else: ?>
        <ul class="g-enderecos-wms-lista">
            <?php foreach ($enderecos as $endereco): ?>
                <?php $qtd = (float) ($endereco['quantidade'] ?? 0); ?>
                <li class="g-enderecos-wms-item">
                    <i class="bi bi-geo-alt-fill"></i>
                    <span class="g-enderecos-wms-codigo"><?= htmlspecialchars((string) ($endereco['localizacao_codigo'] ?? '')) ?></span>
                    <span class="g-enderecos-wms-qtd"><?= $qtd > 0 ? htmlspecialchars(number_format($qtd, 2, ',', '.')) : '—' ?></span>
                </li>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>
</div>

<style>
    .g-enderecos-wms-body { padding: 0.75rem 1.25rem 1.25rem; }
    .g-enderecos-wms-vazio { text-align: center; color: #64748b; font-size: 0.85rem; padding: 1rem 0; }
    .g-enderecos-wms-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
    .g-enderecos-wms-item {
        display: flex; align-items: center; gap: 10px;
        background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);
        border-radius: 8px; padding: 10px 14px; font-size: 0.9rem;
    }
    .g-enderecos-wms-item i { color: #a78bfa; }
    .g-enderecos-wms-codigo { flex: 1; font-weight: 700; color: #f1f5f9; }
    .g-enderecos-wms-qtd { font-weight: 800; color: #a78bfa; }
</style>
```

- [ ] **Step 6: Rodar o teste e confirmar que passa**

Run: `php componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsModalTddTest.php`
Expected: `Tests passed` (Passou: 6 | Falhou: 0)

- [ ] **Step 7: Lint de sintaxe PHP**

Run: `php -l componentes/Missoes/Logistica/sinais/App/App.php && php -l componentes/Missoes/Logistica/sinais/App/html/shared/wms/modal_enderecos_produto.php`
Expected: `No syntax errors detected` para os dois arquivos

- [ ] **Step 8: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/App.php componentes/Missoes/Logistica/sinais/App/html/shared/wms/modal_enderecos_produto.php componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsModalTddTest.php
git commit -m "feat(app): rota de modal enderecosProdutoWms (lista endereços WMS de um produto)"
```

---

### Task 4: Ícone plugável no card (fila única) + wiring em `execucao_card.php`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/App/html/shared/components/wms/enderecos_produto_wms.php`
- Modify: `componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.php` (dentro do bloco `if ($tipo === 'separacao')`, logo após a chamada existente a `separacaoEnderecoWmsBadge()`)
- Test: `componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsBadgeTddTest.php`

**Interfaces:**
- Consumes: `EnderecosProdutoWmsLaboratorio::resolverMapa(array $produtoIds): array` (Task 2); rota `enderecosProdutoWms` (Task 3).
- Produces: função global `enderecosProdutoWmsBadge(array $config): void` — pluga em qualquer card que passe `['item' => $item, 'galaxia' => $galaxia, 'modo_dinamico' => bool]`.

- [ ] **Step 1: Escrever o teste (falhando)**

Criar `componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsBadgeTddTest.php`:

```php
<?php

/**
 * TDD — Ícone "Endereços" no card de Separação (fila única).
 *
 * Testes de inspeção de fonte:
 * 1. enderecos_produto_wms.php declara enderecosProdutoWmsBadge() e fica
 *    inerte quando modo_dinamico=true (fora de escopo desta entrega)
 * 2. execucao_card.php inclui o componente e chama a função dentro do bloco
 *    de separação, repassando item/galaxia/modo_dinamico
 */

class AssertEnderecosWmsBadge
{
    public static int $passou = 0;
    public static int $falhas = 0;

    public static function verdadeiro(bool $condicao, string $msg): void
    {
        if ($condicao) {
            self::$passou++;
            echo "✅ [PASS] $msg\n";
        } else {
            self::$falhas++;
            echo "❌ [FAIL] $msg\n";
        }
    }

    public static function contemTexto(string $src, string $trecho, string $msg): void
    {
        self::verdadeiro(strpos($src, $trecho) !== false, $msg);
    }

    public static function summary(): void
    {
        echo "\n--- RESUMO ---\n";
        echo "Passou: " . self::$passou . " | Falhou: " . self::$falhas . "\n";
        if (self::$falhas > 0) exit(1);
        echo "Tests passed\n";
    }
}

$badgeFile = '/home/goldie/www/genesis/componentes/Missoes/Logistica/sinais/App/html/shared/components/wms/enderecos_produto_wms.php';
$cardFile  = '/home/goldie/www/genesis/componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.php';

echo "Iniciando Testes: EnderecosProdutoWmsBadgeTddTest\n\n";
echo "── enderecos_produto_wms.php ─────────────────────────────────\n";

AssertEnderecosWmsBadge::verdadeiro(file_exists($badgeFile), 'Arquivo enderecos_produto_wms.php existe');

if (file_exists($badgeFile)) {
    $srcBadge = file_get_contents($badgeFile);
    AssertEnderecosWmsBadge::contemTexto($srcBadge, 'function enderecosProdutoWmsBadge(',
        'Declara enderecosProdutoWmsBadge()');
    AssertEnderecosWmsBadge::contemTexto($srcBadge, "config['modo_dinamico']",
        'Verifica modo_dinamico antes de desenhar');
    AssertEnderecosWmsBadge::contemTexto($srcBadge, 'EnderecosProdutoWmsLaboratorio',
        'Usa EnderecosProdutoWmsLaboratorio para resolver os endereços');
    AssertEnderecosWmsBadge::contemTexto($srcBadge, "abrirModal(",
        'Botão usa o mecanismo abrirModal() do framework');
}

echo "\n── execucao_card.php — wiring ────────────────────────────────\n";

$srcCard = file_get_contents($cardFile);

AssertEnderecosWmsBadge::contemTexto($srcCard, "enderecos_produto_wms.php",
    'execucao_card.php faz require_once de enderecos_produto_wms.php');
AssertEnderecosWmsBadge::contemTexto($srcCard, 'enderecosProdutoWmsBadge([',
    'execucao_card.php chama enderecosProdutoWmsBadge()');

// A chamada nova deve estar dentro do bloco if ($tipo === 'separacao') — mesmo
// bloco onde separacaoEnderecoWmsBadge() já é chamado.
$posBlocoSeparacao = strpos($srcCard, "if (\$tipo === 'separacao'):");
$posChamadaBadge   = strpos($srcCard, 'enderecosProdutoWmsBadge([', $posBlocoSeparacao !== false ? $posBlocoSeparacao : 0);
AssertEnderecosWmsBadge::verdadeiro(
    $posBlocoSeparacao !== false && $posChamadaBadge !== false && $posChamadaBadge > $posBlocoSeparacao,
    'Chamada de enderecosProdutoWmsBadge() está dentro do bloco if ($tipo === \'separacao\')'
);

AssertEnderecosWmsBadge::summary();
```

- [ ] **Step 2: Rodar o teste e confirmar que falha**

Run: `php componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsBadgeTddTest.php`
Expected: falhas em todas as verificações do componente (arquivo ainda não existe) e no wiring de `execucao_card.php`

- [ ] **Step 3: Criar o componente plugável**

Criar `componentes/Missoes/Logistica/sinais/App/html/shared/components/wms/enderecos_produto_wms.php`:

```php
<?php

/**
 * Partial: Ícone "Endereços" no card de item da Separação (fila única) — abre
 * um modal (abrirModal()) listando os endereços WMS onde o produto está
 * registrado (saldo local) e a quantidade em cada.
 *
 * View "burra": recebe o item + galaxia, resolve via
 * EnderecosProdutoWmsLaboratorio e só desenha. Sem endereços registrados ->
 * não desenha nada (sem ícone, sem clique em vazio).
 *
 * [Escopo desta entrega] Só suportado no modo "fila única"/Luz (card
 * re-renderizado em PHP a cada item). No modo livre (separacao/item.php,
 * troca de item 100% client-side, `$config['modo_dinamico']=true`) fica
 * inerte por ora — sem re-render por item não há como decidir mostrar/
 * esconder sem um batch precomputado no Presenter, fora de escopo desta
 * entrega.
 *
 * @param array $config [
 *   'item'          => object,          // item atual da missão (precisa de ->produto_id)
 *   'galaxia'       => GalaxiaRoute|null,
 *   'modo_dinamico' => bool,             // true = modo livre — função fica inerte
 * ]
 */
function enderecosProdutoWmsBadge(array $config): void
{
    if (!empty($config['modo_dinamico'])) {
        return;
    }

    $item      = $config['item'] ?? null;
    $galaxia   = $config['galaxia'] ?? null;
    $produtoId = (int) ($item->produto_id ?? 0);

    if (!$item || $produtoId <= 0 || !$galaxia) {
        return;
    }

    $laboratorio = new \GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios\EnderecosProdutoWmsLaboratorio();
    $mapa        = $laboratorio->resolverMapa([$produtoId]);
    $enderecos   = $mapa[$produtoId] ?? [];

    if (empty($enderecos)) {
        return;
    }

    $produtoNome = (string) ($item->produto_nome ?? '');
    $sinal = 'enderecosProdutoWms?produto_id=' . $produtoId . '&produto_nome=' . rawurlencode($produtoNome);
    ?>
    <button type="button" class="carga-badge-enderecos-wms"
        style="background: rgba(139, 92, 246, 0.15); border: 1px solid rgba(139, 92, 246, 0.3); color: #a78bfa; padding: 6px 14px; border-radius: 6px; font-weight: 800; font-size: 0.85rem; display: inline-flex; align-items: center; gap: 6px; text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer;"
        <?= $galaxia->abrirModal($sinal, ['startSinal' => 'App'])['html']; ?>
        title="<?= htmlspecialchars(show('Ver endereços deste produto')) ?>">
        <i class="bi bi-geo-alt-fill"></i> <?= htmlspecialchars(show('Endereços')) ?> (<?= count($enderecos) ?>)
    </button>
    <?php
}
```

- [ ] **Step 4: Ligar o componente em `execucao_card.php`**

Em `componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.php`, linhas 276-289, o bloco atual é exatamente:

```php
                    separacaoEnderecoWmsBadge([
                        'item' => $item,
                        'dados_adicionais' => $dadosExtra,
                        'missao_id' => $missaoId,
                        'galaxia' => $galaxia,
                        'localizacao_legado' => $localizacao,
                        // [PRD separacao-endereco-wms, ciclo 1 — M3] Repassa a flag do
                        // caller (separacao/item.php, modo livre) — default false, então
                        // fila única/carregamento/pesagem/etc. (que nunca passam essa
                        // chave) continuam 100% no branch clássico, intocado.
                        'modo_dinamico' => $config['modo_dinamico'] ?? false,
                    ]);
                    ?>
                <?php endif; ?>
                </div>
```

Note que o `<?php endif; ?>` da linha 289 fecha o `if ($tipo === 'separacao'):` da linha 242 (não um `if` interno) — a nova chamada precisa entrar **antes** dele, ainda dentro do PHP aberto na linha 268 (`<?php`). Substituir esse trecho por:

```php
                    separacaoEnderecoWmsBadge([
                        'item' => $item,
                        'dados_adicionais' => $dadosExtra,
                        'missao_id' => $missaoId,
                        'galaxia' => $galaxia,
                        'localizacao_legado' => $localizacao,
                        // [PRD separacao-endereco-wms, ciclo 1 — M3] Repassa a flag do
                        // caller (separacao/item.php, modo livre) — default false, então
                        // fila única/carregamento/pesagem/etc. (que nunca passam essa
                        // chave) continuam 100% no branch clássico, intocado.
                        'modo_dinamico' => $config['modo_dinamico'] ?? false,
                    ]);

                    // [NOVO] Ícone "Endereços" — lista os endereços WMS (saldo local) do
                    // produto atual e a quantidade em cada um. Só fila única (ver
                    // doc-comment de enderecos_produto_wms.php); inerte no modo livre.
                    if (!function_exists('enderecosProdutoWmsBadge')) {
                        require_once __DIR__ . '/../wms/enderecos_produto_wms.php';
                    }
                    enderecosProdutoWmsBadge([
                        'item' => $item,
                        'galaxia' => $galaxia,
                        'modo_dinamico' => $config['modo_dinamico'] ?? false,
                    ]);
                    ?>
                <?php endif; ?>
                </div>
```

- [ ] **Step 5: Rodar o teste e confirmar que passa**

Run: `php componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsBadgeTddTest.php`
Expected: `Tests passed` (Passou: 6 | Falhou: 0)

- [ ] **Step 6: Lint de sintaxe PHP**

Run: `php -l componentes/Missoes/Logistica/sinais/App/html/shared/components/wms/enderecos_produto_wms.php && php -l componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.php`
Expected: `No syntax errors detected` para os dois arquivos

- [ ] **Step 7: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/html/shared/components/wms/enderecos_produto_wms.php componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.php componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsBadgeTddTest.php
git commit -m "feat(separacao): ícone plugável de Endereços WMS no card de item (fila única)"
```

---

### Task 5: Verificação manual na tela real

**Files:** nenhum (apenas verificação funcional — não há suíte de UI automatizada para o pipeline "fila única" além dos testes de inspeção de fonte já rodados nas Tasks 3-4).

- [ ] **Step 1: Rodar todos os testes automatizados das Tasks 1-4 juntos**

Run:
```bash
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaMapaSaldoPorProdutoTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/EnderecosProdutoWmsLaboratorioTddTest.php
php componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsModalTddTest.php
php componentes/Missoes/Logistica/sinais/App/Testes/Html/EnderecosProdutoWmsBadgeTddTest.php
```
Expected: todos com `TODOS OS TESTES PASSARAM` / `Tests passed`

- [ ] **Step 2: Verificação manual na tela de Separação (fila única/Luz)**

Abrir uma missão de Separação que use o pipeline "fila única" (`luz/separacao/item_card.php`) com um produto que já tenha ao menos um movimento `wms_movimentos` confirmado (ex.: produto usado em uma missão `ALOCACAO_WMS` anterior no mesmo tenant). Verificar:
1. O ícone "Endereços (N)" aparece na área de badges do card, ao lado do badge de endereço-esperado/gôndola.
2. Ao clicar, abre o modal listando cada endereço e a quantidade, ordenados do maior para o menor saldo.
3. Navegar para um produto **sem** nenhum movimento confirmado — o ícone não aparece.
4. Navegar de volta a um produto **com** movimento — o ícone reaparece corretamente (confirma que não há vazamento de estado entre renders).

- [ ] **Step 3: Reportar ao usuário**

Confirmar ao usuário que a verificação manual foi feita (ou que ficou pendente, se não houver acesso a uma missão de teste no momento) antes de considerar a entrega concluída.
