using System.Net;
using System.Net.Http.Json;
using BigRegister.Api.Contracts;
using BigRegister.Api.Data;
using Microsoft.AspNetCore.Mvc.Testing;

namespace BigRegister.Tests;

/// <summary>
/// Org templates (WP-23): admin-only endpoints, draft→publish versioning, and the
/// sent-brief immutability invariant (pin at send, republish touches unsent only).
/// Same reset discipline as BriefEndpointTests — the stores are process-global.
/// </summary>
public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
{
  private const string Registers = OrgTemplateSeed.Registers;
  private readonly HttpClient _client = factory.CreateClient();

  private HttpRequestMessage Req(HttpMethod method, string path, string? role = null, object? body = null)
  {
    var req = new HttpRequestMessage(method, path);
    if (role is not null) req.Headers.Add("X-Role", role);
    if (body is not null) req.Content = JsonContent.Create(body);
    return req;
  }

  private async Task<OrgTemplateAdminViewDto> AdminView(string subOrgId = Registers)
  {
    var res = await _client.SendAsync(Req(HttpMethod.Get, $"/api/v1/admin/org-template/{subOrgId}", role: "admin"));
    res.EnsureSuccessStatusCode();
    return (await res.Content.ReadFromJsonAsync<OrgTemplateAdminViewDto>())!;
  }

  private static void ResetStores()
  {
    OrgTemplateStore.Reset();
    BriefStore.Reset();
  }

  [Fact]
  public async Task Admin_endpoints_are_admin_only()
  {
    ResetStores();
    // drafter (no header) and approver both bounce off every admin endpoint.
    Assert.Equal(HttpStatusCode.Forbidden,
        (await _client.GetAsync("/api/v1/admin/org-templates")).StatusCode);
    Assert.Equal(HttpStatusCode.Forbidden,
        (await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "approver"))).StatusCode);

    var res = await _client.SendAsync(Req(HttpMethod.Get, "/api/v1/admin/org-templates", role: "admin"));
    res.EnsureSuccessStatusCode();
    var list = await res.Content.ReadFromJsonAsync<List<SubOrgSummaryDto>>();
    Assert.Equal(new[] { Registers, OrgTemplateSeed.Vakbekwaamheid }, list!.Select(s => s.SubOrgId));
    Assert.All(list!, s => Assert.Equal(1, s.PublishedVersion));
  }

  [Fact]
  public async Task Publish_increments_version_appends_history_and_counts_unsent_briefs()
  {
    ResetStores();
    // One unsent brief for this sub-org (GetOrCreate on first read).
    await _client.GetAsync("/api/v1/brief");

    var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
    res.EnsureSuccessStatusCode();
    var published = await res.Content.ReadFromJsonAsync<PublishOrgTemplateResponse>();
    Assert.Equal(2, published!.Version);
    Assert.Equal(1, published.AffectedUnsentBriefs);

    var view = await AdminView();
    Assert.Equal(2, view.PublishedVersion);
    Assert.Equal(new[] { 1, 2 }, view.History.Select(h => h.Version));
    Assert.Equal(1, view.UnsentBriefs);
  }

  [Fact]
  public async Task Save_draft_validates_margins_and_round_trips()
  {
    ResetStores();
    var draft = (await AdminView()).Draft;

    var invalid = draft with { Margins = new MarginsDto(5, 20, 20, 25) };
    Assert.Equal(HttpStatusCode.BadRequest, (await _client.SendAsync(
        Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin",
            body: new SaveOrgTemplateRequest(invalid)))).StatusCode);

    var valid = draft with { OrgName = "BIG-register (nieuw)", Margins = new MarginsDto(30, 20, 20, 25) };
    var res = await _client.SendAsync(Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin",
        body: new SaveOrgTemplateRequest(valid)));
    res.EnsureSuccessStatusCode();
    var view = await res.Content.ReadFromJsonAsync<OrgTemplateAdminViewDto>();
    Assert.Equal("BIG-register (nieuw)", view!.Draft.OrgName);
    Assert.Equal(30, view.Draft.Margins.TopMm);
    // Saving a draft publishes nothing.
    Assert.Equal(1, view.PublishedVersion);
  }

  [Fact]
  public async Task Rollback_copies_an_old_version_into_the_draft_without_rewriting_history()
  {
    ResetStores();
    var draft = (await AdminView()).Draft;
    await _client.SendAsync(Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin",
        body: new SaveOrgTemplateRequest(draft with { OrgName = "Versie twee" })));
    await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));

    var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/rollback/1", role: "admin"));
    res.EnsureSuccessStatusCode();
    var view = await res.Content.ReadFromJsonAsync<OrgTemplateAdminViewDto>();
    Assert.Equal("BIG-register", view!.Draft.OrgName); // v1 content back in the draft
    Assert.Equal(2, view.PublishedVersion);            // still live: v2 (rollback ≠ publish)
    Assert.Equal(new[] { 1, 2 }, view.History.Select(h => h.Version)); // append-only, untouched

    Assert.Equal(HttpStatusCode.NotFound, (await _client.SendAsync(
        Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/rollback/99", role: "admin"))).StatusCode);
  }

  [Fact]
  public async Task Sent_brief_keeps_its_pinned_template_while_an_unsent_brief_follows_a_republish()
  {
    ResetStores();
    // Walk one brief to sent under template v1.
    var brief = (await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief"))!.Brief;
    var filled = brief.Sections
        .Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required,
            s.Required && s.Blocks.Count == 0
                ? new[] { new LetterBlockDto("freeText", "b1", new RichTextBlockDto(new[] { new ParagraphDto(new[] { new RichTextNodeDto("text", Text: "inhoud") }) })) }
                : s.Blocks))
        .ToList();
    await _client.PutAsJsonAsync("/api/v1/brief", new SaveBriefRequest(filled));
    await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/submit"));
    await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/approve", role: "approver"));
    var sent = await (await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/send"))).Content.ReadFromJsonAsync<BriefViewDto>();
    Assert.Equal(1, sent!.OrgTemplate.Version);

    // Republish with a new org name.
    var draft = (await AdminView()).Draft;
    await _client.SendAsync(Req(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}", role: "admin",
        body: new SaveOrgTemplateRequest(draft with { OrgName = "Hertitelde organisatie" })));
    await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));

    // The sent brief still renders v1 with the old name (immutable)...
    var sentView = await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief");
    Assert.Equal(1, sentView!.OrgTemplate.Version);
    Assert.Equal("BIG-register", sentView.OrgTemplate.OrgName);

    // ...while a fresh (unsent) brief follows the new published version.
    var freshView = await (await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/reset"))).Content.ReadFromJsonAsync<BriefViewDto>();
    Assert.Equal(2, freshView!.OrgTemplate.Version);
    Assert.Equal("Hertitelde organisatie", freshView.OrgTemplate.OrgName);
  }

  [Fact]
  public async Task Me_returns_the_orgtemplate_capability_for_admin()
  {
    var res = await _client.SendAsync(Req(HttpMethod.Get, "/api/v1/me", role: "admin"));
    var me = await res.Content.ReadFromJsonAsync<MeDto>();
    Assert.Equal(new[] { "orgtemplate:edit" }, me!.Capabilities);
  }

  [Fact]
  public async Task Admin_cannot_slip_into_the_brief_review_flow()
  {
    ResetStores();
    var brief = (await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief"))!.Brief;
    var filled = brief.Sections
        .Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required,
            s.Required && s.Blocks.Count == 0
                ? new[] { new LetterBlockDto("freeText", "b1", new RichTextBlockDto(new[] { new ParagraphDto(new[] { new RichTextNodeDto("text", Text: "inhoud") }) })) }
                : s.Blocks))
        .ToList();
    await _client.PutAsJsonAsync("/api/v1/brief", new SaveBriefRequest(filled));
    await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/submit"));

    // Approve/reject require the Approver role explicitly — admin passes SoD
    // (different identity) but must still be Forbidden.
    Assert.Equal(HttpStatusCode.Forbidden,
        (await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/approve", role: "admin"))).StatusCode);
  }
}
