using System.Reflection;
using Morph.AiKit.Media;
using Xunit;
namespace Morph.AiKit.Tests;
///
/// Canários da MARCAÇÃO — a quarta afirmação da rubrica `media-claims`, e a última que este
/// kit ainda prendia só por prosa.
///
///
///
/// A assimetria que estes testes fecham, medida: a regra do turno de conversa estava
/// presa por TIPO (`internal` + id de job em vez de bytes) e provada por mutação; custo e
/// revisão humana, por ESTADO. A marcação — que é a obrigação legal, o art. 50 do AI Act que
/// motivou a issue — estava escrita em `MediaJobState.cs` ("publicado, com a marcacao
/// aplicada") e em `MediaRequest.cs` ("marcacao visivel obrigatoria"), e um `grep` por label,
/// watermark ou disclosure no kit inteiro voltava vazio. Zero mecanismo.
///
///
/// Uma rubrica que cobra de terceiros uma prova que o próprio exemplar não dá é o defeito que
/// esta epic catalogou. Estes testes são o inverso literal dele.
///
///
/// Fronteira honesta: nada aqui prova que o rótulo chegou aos olhos de alguém — a
/// renderização é do aplicativo consumidor, e nenhum tipo em C# alcança um pixel. O que se
/// prova é que a publicação não acontece sem a decisão registrada, com texto e
/// superfície nomeados.
///
///
public sealed class MediaDisclosureTests
{
private static readonly DateTimeOffset T0 = new(2026, 9, 8, 12, 0, 0, TimeSpan.Zero);
private static MediaDisclosure Label => MediaDisclosure.Create("Imagem gerada por IA", "legenda", T0);
/// Um job aprovado, na finalidade pedida.
private static MediaJob Approved(MediaPurpose purpose)
{
var job = MediaJob.Create(
new MediaRequest("catalogo", "um sofa verde", purpose),
new MediaIdempotencyKey("chave"),
T0).StartGenerating().Value;
job = job.MarkGenerated(
new MediaHash("abc"),
"ab/c/abc.png",
new MediaProvenance("openai", "gpt-image-2", "p", null, [], "-", null, 0m, T0, true)).Value;
return job.Approve(MediaReview.ApprovedBy("lucas", T0)).Value;
}
// ---------------------------------------------------------------------------------
// A política, por finalidade — dado, não comentário
// ---------------------------------------------------------------------------------
/// Catálogo e marketing exigem marcação; rascunho não chega a publicar.
[Theory]
[InlineData(MediaPurpose.CatalogPhoto, true)]
[InlineData(MediaPurpose.Marketing, true)]
[InlineData(MediaPurpose.Draft, false)]
public void RequiresDisclosureIsDeclaredPerPurpose(MediaPurpose purpose, bool expected)
=> Assert.Equal(expected, purpose.RequiresDisclosure());
/// Rascunho é a única finalidade não publicável.
[Theory]
[InlineData(MediaPurpose.CatalogPhoto, true)]
[InlineData(MediaPurpose.Marketing, true)]
[InlineData(MediaPurpose.Draft, false)]
public void OnlyDraftIsUnpublishable(MediaPurpose purpose, bool expected)
=> Assert.Equal(expected, purpose.IsPublishable());
// ---------------------------------------------------------------------------------
// A porta: publicar sem marcação NÃO é um caminho
// ---------------------------------------------------------------------------------
///
/// O caso central. Um job aprovado, de catálogo, sem marcação: não publica, e a
/// razão nomeia o que falta.
///
[Theory]
[InlineData(MediaPurpose.CatalogPhoto)]
[InlineData(MediaPurpose.Marketing)]
public void PublishFailsWithoutADisclosureWhenThePurposeDemandsOne(MediaPurpose purpose)
{
var result = Approved(purpose).Publish(null, T0);
Assert.False(
result.IsSuccess,
"publicar sem marcacao e o caminho que a obrigacao legal existe para fechar; " +
"se ele existe, a afirmacao da rubrica media-claims esta presa so por prosa");
Assert.Equal(MediaErrorCode.InvalidTransition, result.Error!.Code);
Assert.Contains("marcacao", result.Error.Message, StringComparison.OrdinalIgnoreCase);
}
/// Com a marcação, publica — e ela fica GRAVADA no job, não descartada.
[Fact]
public void PublishSucceedsWithADisclosureAndRecordsIt()
{
var published = Approved(MediaPurpose.CatalogPhoto).Publish(Label, T0);
Assert.True(published.IsSuccess);
Assert.Equal(MediaJobState.Published, published.Value.State);
Assert.Equal("Imagem gerada por IA", published.Value.Disclosure!.Text);
Assert.Equal("legenda", published.Value.Disclosure.Surface);
Assert.Equal(T0, published.Value.Disclosure.AppliedAt);
}
///
/// Rascunho não publica, nem com marcação e nem estando aprovado. Era uma frase
/// num comentário XML; agora é uma recusa.
///
[Fact]
public void ADraftNeverPublishesEvenApprovedAndLabelled()
{
var result = Approved(MediaPurpose.Draft).Publish(Label, T0);
Assert.False(result.IsSuccess);
Assert.Contains("nao publica", result.Error!.Message, StringComparison.OrdinalIgnoreCase);
}
///
/// A ordem das portas importa: o estado vem antes da marcação. Um job NÃO aprovado, sem
/// marcação, tem de reclamar da revisão humana — mandar consertar a marcação de algo que
/// nem foi revisado manda consertar a coisa errada.
///
[Fact]
public void TheStateGateIsReportedBeforeTheDisclosureGate()
{
var generated = MediaJob.Create(
new MediaRequest("catalogo", "p", MediaPurpose.CatalogPhoto),
new MediaIdempotencyKey("k"),
T0).StartGenerating().Value
.MarkGenerated(
new MediaHash("abc"), "ab/c/abc.png",
new MediaProvenance("openai", "m", "p", null, [], "-", null, 0m, T0, true)).Value;
var result = generated.Publish(null, T0);
Assert.False(result.IsSuccess);
Assert.Contains("Approved", result.Error!.Message, StringComparison.Ordinal);
Assert.DoesNotContain("marcacao", result.Error.Message, StringComparison.OrdinalIgnoreCase);
}
// ---------------------------------------------------------------------------------
// A marcação vazia é o campo preenchido "para passar"
// ---------------------------------------------------------------------------------
///
/// A construção não tem outra porta. Este é o teste que o nome do anterior prometia
/// e não entregava.
///
///
///
/// A primeira versão de era um record POSICIONAL, e record
/// posicional gera construtor público. A validação vivia só em
/// , então um consumidor externo fazia
/// new MediaDisclosure("", "", at) e publicava com rótulo vazio — medido:
/// IsSuccess=True State=Published. O canário de então exercitava a FÁBRICA e passava
/// verde o tempo todo: media a copia, nao o objeto.
///
///
/// Por isso a asserção é por REFLEXÃO sobre o tipo. Ela não pode ser escrita como
/// new MediaDisclosure(...) — hoje isso nem compila, e é esse o ponto: uma asserção
/// que só existe enquanto o defeito existe não é canário.
///
///
[Fact]
public void TheTypeExposesNoPublicConstructor()
{
var publicCtors = typeof(MediaDisclosure)
.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
Assert.True(
publicCtors.Length == 0,
"MediaDisclosure nao pode ter construtor publico: ele contorna Create e deixa " +
"passar marcacao em branco — o campo preenchido 'para passar' que este tipo existe " +
"para impedir. Record POSICIONAL gera um; use record com construtor privado, como " +
"MediaReview. Encontrados: " + string.Join(" | ", publicCtors.Select(c => c.ToString())));
}
/// Texto ou superfície em branco é recusado pela única porta que existe.
[Theory]
[InlineData("", "legenda")]
[InlineData(" ", "legenda")]
[InlineData("Imagem gerada por IA", "")]
[InlineData("Imagem gerada por IA", " ")]
public void ABlankDisclosureIsRefusedByTheOnlyFactory(string text, string surface)
=> Assert.Throws(() => MediaDisclosure.Create(text, surface, T0));
///
/// A superfície é obrigatória junto do texto: "marcado" sem dizer ONDE não responde se
/// alguém consegue ver.
///
[Fact]
public void TheDisclosureCarriesBothTheTextAndWhereItAppears()
{
var disclosure = MediaDisclosure.Create("Gerado por IA", "sobreposicao", T0);
Assert.Equal("Gerado por IA", disclosure.Text);
Assert.Equal("sobreposicao", disclosure.Surface);
}
/// Uma publicação recusada não muda o job — o registro continua imutável.
[Fact]
public void ARefusedPublishLeavesTheJobUntouched()
{
var job = Approved(MediaPurpose.Marketing);
_ = job.Publish(null, T0);
Assert.Equal(MediaJobState.Approved, job.State);
Assert.Null(job.Disclosure);
Assert.Null(job.PublishedAt);
}
}