using System.Text;
using Morph.AiKit.Media;
using Xunit;
namespace Morph.AiKit.Tests;
///
/// Canarios de : a chave que dedupe a CHAMADA PAGA.
///
///
/// Duas propriedades, e as duas custam dinheiro quando falham. Estabilidade: o mesmo
/// pedido tem de dar a mesma chave, senao todo retry paga de novo. Sensibilidade:
/// qualquer campo participante que mude tem de mudar a chave, senao um pedido reusa o
/// artefato pago de OUTRO — pior que pagar duas vezes.
///
public sealed class MediaIdempotencyKeyTests
{
private static MediaRequest Request(
string prompt = "um sofa verde",
MediaOptions? options = null,
params string[] references)
=> new("catalogo", prompt, MediaPurpose.CatalogPhoto)
{
Options = options ?? MediaOptions.None,
References = [.. references.Select(r =>
new MediaReference(r, "image/png", Encoding.UTF8.GetBytes(r)))],
};
private static string KeyOf(MediaRequest request)
=> MediaIdempotencyKey.From(request, "openai", "gpt-image-2").Value;
// ---------------------------------------------------------------------------------
// Estabilidade
// ---------------------------------------------------------------------------------
/// Duas construcoes do mesmo pedido dao a mesma chave.
[Fact]
public void TheSameRequestAlwaysGivesTheSameKey()
=> Assert.Equal(KeyOf(Request()), KeyOf(Request()));
///
/// Espaco em branco a mais nao muda o pedido, entao nao muda a chave. E o que impede um
/// prompt re-serializado com quebra de linha diferente de virar outra fatura.
///
[Fact]
public void WhitespaceIsNormalisedAway()
=> Assert.Equal(KeyOf(Request("um sofa verde")), KeyOf(Request(" um sofa\n verde ")));
///
/// A caixa NAO e normalizada, e e deliberado: num prompt ela muda o resultado (nome de
/// marca, texto literal que vai aparecer na imagem).
///
[Fact]
public void CaseIsPreservedBecauseItChangesThePicture()
=> Assert.NotEqual(KeyOf(Request("um sofa VERDE")), KeyOf(Request("um sofa verde")));
/// A chave e um sha256 em hex minusculo: 64 digitos.
[Fact]
public void TheKeyIsASha256InLowercaseHex()
{
var key = KeyOf(Request());
Assert.Equal(64, key.Length);
Assert.Matches("^[0-9a-f]{64}$", key);
}
// ---------------------------------------------------------------------------------
// Sensibilidade — uma rodada POR CAMPO participante
// ---------------------------------------------------------------------------------
/// Prompt diferente, chave diferente.
[Fact]
public void ADifferentPromptChangesTheKey()
=> Assert.NotEqual(KeyOf(Request("um sofa azul")), KeyOf(Request()));
/// Modelo diferente, chave diferente — o alias pode ser repontado.
[Fact]
public void ADifferentModelChangesTheKey()
=> Assert.NotEqual(
MediaIdempotencyKey.From(Request(), "openai", "gpt-image-2").Value,
MediaIdempotencyKey.From(Request(), "openai", "outro-modelo").Value);
/// Provider diferente, chave diferente.
[Fact]
public void ADifferentProviderChangesTheKey()
=> Assert.NotEqual(
MediaIdempotencyKey.From(Request(), "openai", "m").Value,
MediaIdempotencyKey.From(Request(), "google", "m").Value);
///
/// Trocar a foto de referencia E outro pedido, mesmo com o prompt identico. Por
/// isso o sha256 de CADA referencia participa, e nao a contagem delas.
///
[Fact]
public void ADifferentReferenceChangesTheKey()
=> Assert.NotEqual(
KeyOf(Request(references: ["foto-a"])),
KeyOf(Request(references: ["foto-b"])));
/// Mesma quantidade de referencias, conteudo trocado de ordem: outro pedido.
[Fact]
public void TheOrderOfReferencesIsPartOfTheRequest()
=> Assert.NotEqual(
KeyOf(Request(references: ["a", "b"])),
KeyOf(Request(references: ["b", "a"])));
/// Sem referencia e com referencia sao pedidos diferentes.
[Fact]
public void AddingAReferenceChangesTheKey()
=> Assert.NotEqual(KeyOf(Request()), KeyOf(Request(references: ["foto-a"])));
/// Cada opcao participante, uma rodada.
[Theory]
[MemberData(nameof(OptionVariations))]
public void EveryParticipatingOptionChangesTheKey(MediaOptions variation)
=> Assert.NotEqual(KeyOf(Request()), KeyOf(Request(options: variation)));
/// As variacoes de opcao exercitadas, uma por campo canonico.
public static TheoryData OptionVariations() =>
[
new MediaOptions { Quality = MediaQuality.High },
new MediaOptions { Count = 4 },
new MediaOptions { Width = 1024, Height = 1024 },
new MediaOptions { MediaType = "image/webp" },
new MediaOptions { DurationSeconds = 8 },
new MediaOptions { AspectRatio = "9:16" },
];
///
/// O separador e um caractere de CONTROLE, e nao |. Um prompt que contivesse o
/// separador visivel emendaria campos e faria dois pedidos diferentes colidirem — que
/// e o defeito que faz um pedido reusar o artefato pago do outro.
///
[Fact]
public void APromptFullOfPipesDoesNotCollideWithAnotherRequest()
=> Assert.NotEqual(
KeyOf(Request("gpt-image-2|Image|um sofa")),
KeyOf(Request("um sofa")));
/// Normalizacao de prompt e pura e conferivel sozinha.
[Theory]
[InlineData(" a b ", "a b")]
[InlineData("a\n\nb", "a b")]
[InlineData("", "")]
[InlineData(" ", "")]
public void NormalizePromptCollapsesWhitespace(string input, string expected)
=> Assert.Equal(expected, MediaIdempotencyKey.NormalizePrompt(input));
}