# Revisões — Estratégia de Testes

> O legado **não possui testes do módulo de Revisões**. A única cobertura existente é
> **indirecta**, no widget de dashboard:
> `v6/tests/Feature/RevisoesWidgetTest.php` e `v6/tests/Feature/DashboardRevisoesWidgetTest.php`
> (que exercitam `IEnviosRepository::returnEnvios()` via `App\Services\Dashboard\Widgets\RevisoesWidget`).
> Todo o restante (avaliação, perguntas, respostas, PDF, e-mail) é **não testado**.

---

## 1. Estratégia proposta

| Camada | Ferramenta | Cobertura |
|---|---|---|
| **Unit** | PHPUnit + `Tests\TestCase` | Cálculo de pontuação, geração de protocolo, serialização do JSON do questionário, montagem dos dados dos gráficos |
| **Feature (HTTP)** | PHPUnit + `Tests\TestCase` + `DatabaseTransactions` | Rotas do CMS: CRUD da avaliação, perguntas, categorias, opções, resposta, e-mail, relatórios, permissões |
| **Feature (widget)** | `Tests\DashboardTestCase` (já existente) | Regressão do widget "Últimas Revisões" (já coberto) |
| **Integração (PDF)** | Teste manual / golden file | Layout do PDF gerado em `EnviosRepository::generatePDF()` |

Base de dados de teste: a mesma configuração do `phpunit.xml`
(`v6/phpunit.xml`: `CACHE_DRIVER=array`, `SESSION_DRIVER=array`, `QUEUE_DRIVER=sync`).
O `DatabaseTransactions` do `DashboardTestCase` garante rollback.

## 2. Como rodar

```bash
# a partir de v6/
composer install
./vendor/bin/phpunit --testsuite Unit
./vendor/bin/phpunit --testsuite Feature

# um arquivo específico
./vendor/bin/phpunit tests/Feature/Revisoes/ReviewSubmissionTest.php

# filtrar por nome
./vendor/bin/phpunit --filter testOnlySimOptionScoresPoints
```

Rodar os testes de Revisões no legado (v6) **não** é possível sem antes executar as
migrations de maio/2022 (tabelas `ratings`, `questions`, `question_types`,
`question_categories`, `option_categories`, `options`, `option_category_has_options`,
`send_ratings` e `answer_forms`) na base de teste.

## 3. Fixtures necessárias

Factories a criar (padrão Laravel, em `v6/database/factories/`):

```php
RatingFactory::class        // title, subtitle, description, slug, status
QuestionTypeFactory::class  // title = 'Resposta única'
QuestionCategoryFactory::class
OptionCategoryFactory::class
OptionFactory::class        // description = 'Sim' | 'Não' | 'Parcial'
QuestionFactory::class      // value (peso), order,FKs
SendRatingFactory::class    // revisor, data, status, protocol, pts_finais, pts_totais
AnswerFormFactory::class    // option_id, observation
EmailRevisaoFactory::class  // title = e-mail
```

Estado do banco necessário para os testes:

| Tabela | Origem |
|---|---|
| `question_types` | `v6/database/seeds/QuestionTypesSeeder.php` (id 1 = "Resposta única") |
| `option_categories` | `v6/database/seeds/OptionCategoriesSeeder.php` (id 1 = "Comum") |
| `options` | `v6/database/seeds/OptionsSeeder.php` (id 1 = "Sim", 2 = "Não", 3 = "Parcial") |
| `option_category_has_options` | `v6/database/seeds/OptionsCategoryHasOptionSeeder.php` |
| `ratings`, `question_categories`, `questions` | **não** usar `PrefeituraRevSeeder`/`CamaraRevSeeder` nos testes (ids fixos e volumes grandes) — usar factories |

Seed mínimo para o ambiente de teste:

```php
// novo arquivo RevisoesTestSeeder.php, em v6/database/seeds/
$this->call(QuestionTypesSeeder::class);
$this->call(OptionsSeeder::class);
$this->call(OptionCategoriesSeeder::class);
$this->call(OptionsCategoryHasOptionSeeder::class);
```

> Os seeders oficiais executam um `delete()` sem filtro na tabela antes de inserir: rodá-los
> em uma base de teste compartilhada apaga os dados de outros testes.

## 4. Casos de teste (Arrange / Act / Assert)

### 4.1 Avaliação (`ratings`)

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| A1 | Cria avaliação com slug derivado do título | POST autenticado com `title = "Prefeitura Municipal"` | `POST /cms/revisoes` | 302 para `revisoes.edit`; `ratings.slug = "prefeitura-municipal"`; `ratings.status = false` |
| A2 | Título, subtítulo e descrição são obrigatórios | POST sem `subtitle` | `POST /cms/revisoes` | 422 / redirect com erro de validação; nenhuma linha criada |
| A3 | Ativar uma avaliação desativa as anteriores | 2 avaliações, uma com `status = true` | `POST /cms/revisoes` com `status` marcado | a antiga fica `status = false`, a nova `status = true`; **apenas 1 ativa** |
| A4 | `setStatus` alterna o estado | avaliação `status = false` | `POST /cms/setStatus` com `contents_id` | `status = true` e as demais desativadas |
| A5 | Cópia duplica as perguntas, mas não as categorias | avaliação com 2 perguntas e 1 categoria | `GET /cms/copiar_revisao/{id}` | nova avaliação com título "(copia)" e `status = false`; 2 perguntas copiadas apontando para as **categorias da original** |
| A6 | Export JSON gera arquivo em `public/cache` | avaliação com perguntas | `GET /cms/download_revisao/{id}` | 200, `Content-Type: application/json`; arquivo `public/cache/revisao_{slug}.json` existe |
| A7 | Import JSON recria a avaliação | JSON exportado no caso A6 | `POST /cms/import_revisao` | 200 com o campo `success` preenchido; nova avaliação `status = false` com as perguntas importadas |
| A8 | Import não reaproveita categorias de opções já existentes | JSON com categoria de opções "Comum" | `POST /cms/import_revisao` | **nenhuma** opção nova criada (as do JSON são descartadas) |
| A9 | Exclusão em lote registra atividade | 2 avaliações selecionadas | `POST /cms/ajax/revisoes/delete-por-lote` | ambas soft-deleted; activity log com "Exclusão de revisões" |

### 4.2 Motor de questionário

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| Q1 | Cria pergunta com peso | POST `description`, `value`, FKs | `POST /cms/questions/saveQuestion` | linha em `questions`; resposta JSON contém as relações carregadas |
| Q2 | Edita pergunta | pergunta existente | `POST /cms/questions/editQuestion` | campos atualizados; `order` preservado |
| Q3 | Exclui pergunta (soft delete) | pergunta existente | `POST /cms/questions/deleteQuestion` | `deleted_at` preenchido; `questions` ainda referencia a FK |
| Q4 | Reordena perguntas | 3 perguntas com `order` 1,2,3 | `POST /cms/reorder` com `ordem = [id3, id1, id2]` | `order` = 1,2,3 conforme a ordem recebida |
| Q5 | Reordena categorias | 2 categorias | `POST /cms/categories-reorder` | `order` atualizado |
| Q6 | Cria categoria filha | categoria raiz existente | `POST /cms/options/saveCategories` com `question_categories_id` | `question_categories.question_categories_id` = id da raiz |
| Q7 | Não exclui categoria com perguntas | categoria usada por 1 pergunta | `POST /cms/options/deleteCategories` | resposta de erro ("vinculada a uma pergunta"); categoria intacta |
| Q8 | Não exclui categoria com filhas | categoria com 1 subcategoria | `POST /cms/options/deleteCategories` | resposta de erro ("vinculada a outra categoria") |
| Q9 | Cria grupo de opções com alternativas | POST `title` + `selectedOptions` | `POST /cms/options/saveOptionsCategories` | `option_category_has_options` com N linhas |
| Q10 | Exclui alternativa que pertence a uma categoria | alternativa ligada | `POST /cms/options/deleteOption` | retorna "false"; alternativa intacta |

### 4.3 Resposta, pontuação e PDF

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| S1 | Registra a avaliação e cria as respostas | avaliação com 2 perguntas (pesos 5 e 10), 1 categoria | `POST /cms/respostas` com `dataDeEnvio`, `revisor`, `resposta[]` | 1 `send_ratings` (status `false`, `protocol` preenchido) + 2 `answer_forms` |
| S2 | **Só "Sim" pontua** | pergunta peso 10 respondida "Sim"; outra peso 5 respondida "Não" | `POST /cms/respostas` | `pts_finais = 10`, `pts_totais = 15` |
| S3 | "Parcial" não pontua | pergunta peso 10 respondida "Parcial" | `POST /cms/respostas` | `pts_finais = 0` |
| S4 | Retificação herda o protocolo | envio existente com `protocol = 2026011234` | `POST /cms/respostas` com `content_id` do envio pai | novo envio com **o mesmo** `protocol` e `send_id` = id do pai |
| S5 | Novo envio gera protocolo próprio | sem `content_id` | `POST /cms/respostas` | `protocol` = "AAAAMM" + 4 dígitos, sem colisão com o padrão |
| S6 | PDF é gerado e gravado | avaliação respondida | `POST /cms/respostas` | arquivo existe em `storage/app/public/content/revisoes/{id}/pdf/`; `send_ratings.path` preenchido |
| S7 | Percentual não quebra com total zero | avaliação sem perguntas (todos os pesos nulos) | `GET /cms/respostas/pdf/{id}` | **não** deve lançar `DivisionByZeroError` (em PHP 8) — hoje gera `INF` |
| S8 | Envio de e-mail marca o envio como enviado | envio com PDF | `POST /cms/resposta/enviar_email` com `emails[]` | `email_revisoes` com 1 linha; `send_ratings.status = true`; `Mail::fake()` registra 1 e-mail com anexo |
| S9 | Envio de e-mail **sobrescreve a data** (bug) | envio com `data = 2026-01-10 10:00` | `POST /cms/resposta/enviar_email` | `data` passa a ser "agora" — comportamento a ser **corrigido** na v7 (`sent_at`) |

### 4.4 Relatórios e listagens

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| R1 | Relatório por categoria | envio com respostas em 2 categorias | `GET /cms/resposta/enviadas/{id}` | 200; view `envios/send_estatistica`; 1 `canvas` por categoria |
| R2 | Relatório anual | envios em 3 meses do ano | `GET /cms/respostas/enviadas/anuais` | 200; view `envios/send_anual` |
| R3 | DataTable de formulários | 2 avaliações | `GET /cms/datatable/formulario` | JSON DataTables com `formulario_name`, `descricao`, `status_name`, `status` |
| R4 | DataTable de envios só lista enviados | 1 envio `status = true`, 1 `status = false` | `GET /cms/datatable/envios` | apenas o envio com `status = true` |
| R5 | DataTable de respostas só lista pendentes | idem | `GET /cms/datatable/respostas` | apenas o envio com `status = false` |
| R6 | Listagem do cliente só mostra a avaliação ativa | avaliação A ativa e B inativa, ambas com envios enviados | `GET /cms/datatable/listagem_revisoes` | apenas os envios da avaliação ativa |

### 4.5 Autorização

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| P1 | Rota de avaliação exige login | visitante | `GET /cms/revisoes` | redirect para `login` |
| P2 | Rota de resposta exige login | visitante | `GET /cms/respostas` | redirect para `login` |
| P3 | Usuário sem permissão é barrado | usuário logado sem `revisoes.*` | `GET /cms/revisoes` | redirect com flash "Você não tem autorização para acessar esta funcionalidade!" (`v6/app/Http/Middleware/CheckRoutePermission.php:26`) |
| P4 | Super-admin passa | usuário `super-admin` | `GET /cms/revisoes` | 200 |

## 5. Testes manuais (cenário de aceite do negócio)

1. Criar a avaliação "Prefeitura" com 3 categorias e 5 perguntas ponderadas.
2. Marcar "Formulário ativo" e confirmar que a avaliação anterior foi desativada.
3. "Iniciar revisão" → preencher data, revisor e respostas (uma "Sim", uma "Não", uma "Parcial").
4. Salvar → conferir `pts_finais`/`pts_totais` (via tela de relatório) e abrir o PDF.
5. Enviar o PDF para dois destinatários → conferir a caixa de entrada e a tela "Lista de Envios".
6. Confirmar que a **data** exibida em "Lista de Envios" é a data do envio do e-mail (bug
   documentado em `05-REGRAS-E-PORTABILIDADE.md`, seção 1.5).
7. Exportar o questionário em JSON, alterar e reimportar → conferir a árvore resultante.
8. Verificar que o widget "Últimas Revisões" do dashboard reflete o envio.

## 6. Cobertura atual e lacunas

| Área | Coberta hoje | Prioridade na v7 |
|---|---|---|
| `RevisoesWidget` (dashboard) | Sim (`v6/tests/Feature/RevisoesWidgetTest.php`, `v6/tests/Feature/DashboardRevisoesWidgetTest.php`) | Manter |
| CRUD de avaliação | Não | Alta |
| Motor de questionário | Não | Alta |
| Pontuação / protocolo | Não | **Alta** (regra de negócio crítica) |
| Resposta + PDF | Não | Alta |
| E-mail com anexo | Não | Média |
| Views SQL das listagens | Não | Média |
| Import/export JSON | Não | Média |
