using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

namespace DodoSSH.Contracts.Tests;

/// <summary>
/// Applying the contract's settings to options this process does not own.
/// </summary>
/// <remarks>
/// The host cannot be handed <c>StrictRequestOptions</c> — ASP.NET Core exposes its serialiser options as
/// a get-only instance already built from the web defaults — so they have to be copied in. Copying is
/// exactly the hazard the context's own documentation warns about, which is why every setting that
/// distinguishes the contract from those defaults is asserted here rather than assumed.
/// <para>
/// The consequence of getting this wrong is not subtle: the web defaults write an enum as a number, and
/// every request DTO carrying one then fails to bind against a real client.
/// </para>
/// </remarks>
public sealed class ApplyToTests
{
    [Fact]
    public void AppliedOptions_WriteEnumsAsStrings()
    {
        // The setting that actually broke. A number here means the sync surface is unreachable.
        var change = new SyncChange(
            SyncEntityType.Host,
            Guid.CreateVersion7(),
            SyncOperation.Upsert,
            Version: 1,
            ChangeSequence: 1,
            Payload: null,
            PlaintextFields: null,
            UpdatedAt: DateTimeOffset.UnixEpoch);

        var json = JsonSerializer.Serialize(change, Applied());

        json.ShouldContain("\"Host\"");
        json.ShouldContain("\"Upsert\"");
        json.ShouldNotContain("\"entityType\":1");
    }

    [Fact]
    public void AppliedOptions_ReadEnumsAsStrings()
    {
        var request = new SyncPullRequest("cursor", 100, [SyncEntityType.Host, SyncEntityType.Credential]);

        var options = Applied();

        var restored = JsonSerializer.Deserialize<SyncPullRequest>(
            JsonSerializer.Serialize(request, options), options);

        restored.ShouldNotBeNull();
        restored.EntityTypes.ShouldBe(request.EntityTypes);
    }

    [Fact]
    public void AppliedOptions_RejectAnUnmappedMember()
    {
        // The documented reason StrictRequestOptions exists: a renamed or misspelled property surfaces as
        // a 400 rather than as a silently missing value that later looks like data loss.
        const string Body = """{"cursor":"c","limit":10,"entityTypes":null,"typo":true}""";

        Should.Throw<JsonException>(() => JsonSerializer.Deserialize<SyncPullRequest>(Body, Applied()));
    }

    [Fact]
    public void AppliedOptions_RejectANumberInAString()
    {
        // JsonSerializerDefaults.Web replaces Strict with AllowReadingFromString, and two implementations
        // that disagree about whether "1" is a number disagree silently.
        const string Body = """
            {"envelope":"AQID","wrappedDataKey":"BAU=",
             "dataKeyId":"0192f0c8-0000-7000-8000-000000000000",
             "keyGeneration":"1","aadVersion":1}
            """;

        Should.Throw<JsonException>(() => JsonSerializer.Deserialize<EncryptedPayload>(Body, Applied()));
    }

    [Fact]
    public void AppliedOptions_UseCamelCase()
    {
        // Compared exactly. Shouldly's ShouldNotContain is case-insensitive, so asserting the absence of
        // "Cursor" would pass on "cursor" and prove nothing.
        JsonSerializer.Serialize(new SyncPullRequest("c", 1, null), Applied())
            .ShouldBe("""{"cursor":"c","limit":1}""");
    }

    [Fact]
    public void AppliedOptions_MatchTheContractsOwnSettings()
    {
        // A blanket comparison, so a setting added to the source-generation attributes and forgotten in
        // ApplyTo fails here rather than at a wire boundary.
        var applied = Applied();
        var contract = DodoSshJsonContext.StrictRequestOptions;

        applied.PropertyNamingPolicy.ShouldBe(contract.PropertyNamingPolicy);
        applied.NumberHandling.ShouldBe(contract.NumberHandling);
        applied.DefaultIgnoreCondition.ShouldBe(contract.DefaultIgnoreCondition);
        applied.UnmappedMemberHandling.ShouldBe(contract.UnmappedMemberHandling);
        applied.PropertyNameCaseInsensitive.ShouldBe(contract.PropertyNameCaseInsensitive);
        applied.DictionaryKeyPolicy.ShouldBe(contract.DictionaryKeyPolicy);
    }

    [Fact]
    public void AppliedOptions_LeaveTheCallersOwnResolverInPlace()
    {
        // The guarantee a host depends on. The context goes in front so contract types use the generated
        // metadata, and whatever the host already installed keeps handling everything else —
        // ProblemDetails among them. Replacing the chain would break every framework type a server writes.
        JsonSerializer.Serialize(new Unrelated("value"), Applied()).ShouldBe("""{"name":"value"}""");
    }

    [Fact]
    public void WithoutAFallbackResolver_TypesOutsideTheContractAreRefused()
    {
        // Stated rather than assumed, because it is the trade being made: a reflection fallback inside
        // ApplyTo would cost trimmability, which is half the reason this context is source-generated.
        // Options carrying no resolver are the caller's problem, loudly rather than silently.
        var bare = new JsonSerializerOptions(JsonSerializerDefaults.Web);
        DodoSshJsonContext.ApplyTo(bare);

        Should.Throw<NotSupportedException>(() => JsonSerializer.Serialize(new Unrelated("v"), bare));

        // Contract types still work, so the failure is narrow rather than total.
        JsonSerializer.Serialize(new SyncPullRequest("c", 1, null), bare).ShouldNotBeNullOrEmpty();
    }

    /// <remarks>
    /// Mirrors ASP.NET Core's own options: web defaults <em>with</em> a reflection resolver already
    /// installed, which is the state <c>ConfigureHttpJsonOptions</c> hands over.
    /// </remarks>
    private static JsonSerializerOptions Applied()
    {
        var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
        {
            TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
        };

        DodoSshJsonContext.ApplyTo(options);

        return options;
    }

    private sealed record Unrelated(string Name);
}
