using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
namespace WordVersioning.Api.IntegrationTests;
/// FR-11 — field sync ตอน Save & Version: latest values อัปเดต + snapshot ต่อ version
/// + missing-field policy (AC-13: ค่าเปลี่ยนตาม, AC-14: field หาย → ค่าเดิม + mark missing ไม่ fail)
[Collection(IntegrationTestCollection.Name)]
public sealed class FieldSyncTests(IntegrationTestFixture fixture)
{
/// สร้างเอกสารจาก template ที่ตั้งค่าแล้ว (v0 ค่า DefaultValues) — จุดตั้งต้นของทุก test ในคลาสนี้
private async Task<(HttpClient Client, Guid DocumentId, Guid V0VersionId)> CreateTrackedDocumentAsync()
{
var client = fixture.CreateClient();
var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "field sync");
var doc = await TestFlow.GenerateDocumentAsync(client, templateId);
return (client, doc.DocumentId, doc.VersionId);
}
/// AC-13 — แก้ค่าใน content control แล้ว Save & Version → latest values เปลี่ยนตาม
/// + snapshot ของ version นั้นเก็บค่าใหม่ และ v0 snapshot ยังเก็บค่าเก่าไว้ (diff ได้)
[Fact]
public async Task UploadWithChangedFields_UpdatesLatestValues_AndSnapshotsPerVersion()
{
var (client, documentId, v0VersionId) = await CreateTrackedDocumentAsync();
var v1Fields = new Dictionary
{
["employee_name"] = "สมหญิง เปลี่ยนชื่อแล้ว",
["position"] = "วิศวกรซอฟต์แวร์", // ไม่เปลี่ยน
["salary"] = "99000",
["start_date"] = "2026-08-01", // ไม่เปลี่ยน
};
using var upload = await client.PostAsync(
$"api/documents/{documentId}/versions",
ApiExtensions.VersionForm(
TestDocx.BuildPlainDocx("แก้ไขใน Word"), "doc.docx", Guid.NewGuid().ToString(),
JsonSerializer.Serialize(v1Fields)));
Assert.Equal(HttpStatusCode.Created, upload.StatusCode);
var v1 = await upload.Content.ReadFromJsonAsync(IntegrationTestFixture.Json);
// latest values: field ที่ "ส่งมา" ถูก update ทุกตัว (แม้ค่าเดิมไม่เปลี่ยน — ไม่มี dirty check)
// จึงชี้ SourceVersionId = v1 ทั้งหมด; ตัวที่ "ไม่ได้ส่ง" เท่านั้นที่คง v0 (ดู test AC-14)
await using var db = fixture.CreateDb();
var values = await db.DocumentFieldValues.AsNoTracking()
.Where(v => v.DocumentId == documentId)
.ToDictionaryAsync(v => v.FieldKey);
Assert.Equal("สมหญิง เปลี่ยนชื่อแล้ว", values["employee_name"].Value);
Assert.Equal(v1!.VersionId, values["employee_name"].SourceVersionId);
Assert.Equal("99000", values["salary"].Value);
Assert.Equal(v1.VersionId, values["salary"].SourceVersionId);
Assert.Equal("วิศวกรซอฟต์แวร์", values["position"].Value); // ค่าเดิมส่งมาเท่าเดิม → คงเดิมแต่ source ขยับตาม v1
Assert.Equal(v1.VersionId, values["position"].SourceVersionId);
Assert.Equal(fixture.TestUserId, values["employee_name"].UpdatedByUserId);
// snapshot ต่อ version ผ่าน history API: v1 = ค่าใหม่, v0 = ค่าเดิม (missing=false ทั้งคู่)
using var history = await client.GetAsync($"api/documents/{documentId}/versions");
var items = (await history.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json))!
.OrderBy(i => i.VersionNumber)
.ToList();
var v0Snapshot = Snapshot(items[0].Fields!.Value);
var v1Snapshot = Snapshot(items[1].Fields!.Value);
Assert.Equal("สมหญิง รักงาน", v0Snapshot["employee_name"].Value);
Assert.Equal("สมหญิง เปลี่ยนชื่อแล้ว", v1Snapshot["employee_name"].Value);
Assert.Equal("99000", v1Snapshot["salary"].Value);
Assert.All(v0Snapshot.Values, e => Assert.False(e.Missing));
Assert.All(v1Snapshot.Values, e => Assert.False(e.Missing));
}
/// AC-14 — ลบ content control แล้ว Save & Version: request สำเร็จ, ค่าเดิมคงอยู่ + mark missing
/// ใน snapshot; field ที่ส่งเกินนิยามถูก ignore
[Fact]
public async Task UploadWithMissingFields_KeepsOldValues_MarksMissing_DoesNotFail()
{
var (client, documentId, v0VersionId) = await CreateTrackedDocumentAsync();
// ผู้ใช้ลบ control ของ position/salary/start_date — เหลือส่ง employee_name เท่านั้น + ส่งเกิน 1 field
using var upload = await client.PostAsync(
$"api/documents/{documentId}/versions",
ApiExtensions.VersionForm(
TestDocx.BuildPlainDocx("ลบ control ไป"), "doc.docx", Guid.NewGuid().ToString(),
"""{"employee_name":"สมหญิง ยังอยู่","อะไรที่ไม่มีในนิยาม":"ส่งเกินมา"}"""));
// policy FR-11: ไม่ fail request
Assert.Equal(HttpStatusCode.Created, upload.StatusCode);
// latest values ของ field ที่หาย: คงค่า v0 เดิมไว้ทั้งหมด (รวม SourceVersionId ที่ยังชี้ v0)
await using var db = fixture.CreateDb();
var values = await db.DocumentFieldValues.AsNoTracking()
.Where(v => v.DocumentId == documentId)
.ToDictionaryAsync(v => v.FieldKey);
Assert.Equal("สมหญิง ยังอยู่", values["employee_name"].Value);
Assert.Equal("วิศวกรซอฟต์แวร์", values["position"].Value);
Assert.Equal(v0VersionId, values["position"].SourceVersionId); // ไม่ถูกแตะเพราะไม่ได้ส่งมา
Assert.Equal("75000", values["salary"].Value);
Assert.Equal("2026-08-01", values["start_date"].Value);
Assert.Equal(4, values.Count); // field เกินไม่ถูกเก็บ
// snapshot v1: field ที่หาย missing=true พร้อมค่าเดิม, ไม่มี key ของ field เกิน
using var history = await client.GetAsync($"api/documents/{documentId}/versions");
var items = await history.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json);
var snapshot = Snapshot(items!.Single(i => i.VersionNumber == 1).Fields!.Value);
Assert.False(snapshot["employee_name"].Missing);
Assert.True(snapshot["position"].Missing);
Assert.Equal("วิศวกรซอฟต์แวร์", snapshot["position"].Value); // ค่าเดิม
Assert.True(snapshot["salary"].Missing);
Assert.True(snapshot["start_date"].Missing);
Assert.Equal(4, snapshot.Count); // ไม่มี "อะไรที่ไม่มีในนิยาม"
}
/// FR-12 ผ่าน detail — หลัง sync แล้วหน้า detail ต้องเห็นค่าล่าสุด (ไม่ใช่ของ v0)
[Fact]
public async Task DetailReflectsLatestSyncedValues()
{
var (client, documentId, _) = await CreateTrackedDocumentAsync();
using var upload = await client.PostAsync(
$"api/documents/{documentId}/versions",
ApiExtensions.VersionForm(
TestDocx.BuildPlainDocx(), "doc.docx", Guid.NewGuid().ToString(),
"""{"employee_name":"ชื่อใหม่สุด","position":"ตำแหน่งใหม่สุด","salary":"1","start_date":"2026-08-20"}"""));
upload.EnsureSuccessStatusCode();
using var detail = await client.GetAsync($"api/documents/{documentId}");
var body = await detail.Content.ReadFromJsonAsync(IntegrationTestFixture.Json);
var byKey = body!.Fields.ToDictionary(f => f.Key);
Assert.Equal("ชื่อใหม่สุด", byKey["employee_name"].Value);
Assert.Equal("ตำแหน่งใหม่สุด", byKey["position"].Value);
Assert.Equal("1", byKey["salary"].Value);
}
/// แปลง snapshot JSON (structured {key:{value,missing}}) เป็น dictionary ใช้ใน test
private static Dictionary Snapshot(JsonElement fields)
{
var result = new Dictionary();
foreach (var prop in fields.EnumerateObject())
result[prop.Name] = (
prop.Value.GetProperty("value").GetString() ?? "",
prop.Value.GetProperty("missing").GetBoolean());
return result;
}
}