using Application.Helpers;
using Domain.Entities.Forum.Briefings;
using Domain.Entities.Stocks;
using Domain.Entities.Stocks.ValueObject;
using Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace Application.Tests;
///
/// D2 M3 장전 브리핑 자동 발행 통합 검증 — DailyBriefingComposer.
/// 발행 시 briefing 보드 lazy 시드 + 시스템 계정(MemberID=null) IsNotice Post + DailyBriefing 기록,
/// BriefingDate 멱등(같은 날 재실행 skip), 본문에 지수/등락/공시 취합을 검증한다.
/// 공유 antooza_test DB — 종목코드는 98xxxx 고범위 + GUID 격리.
///
[TestClass]
public sealed class DailyBriefingTests
{
private static int _codeSeq = 980000;
private static string NextCode() => System.Threading.Interlocked.Increment(ref _codeSeq).ToString("D6");
private static async Task SeedIndexAsync(AppDbContext db, MarketIndexSeries series, string name, DateOnly date, decimal close, decimal changeVal, int flucRateBp)
{
await db.IndexDailyPrice.AddAsync(IndexDailyPrice.Create(series, name, date, close, changeVal, flucRateBp, close, close, close, 1000L, 1_000_000L));
await db.SaveChangesAsync(default);
}
private static async Task SeedStockWithPriceAsync(AppDbContext db, string code, string name, DateOnly date, int close, decimal changeRate)
{
var stock = Stock.Create(code, name, StockMarket.KOSPI, new DateOnly(2020, 1, 2));
await db.Stock.AddAsync(stock);
await db.SaveChangesAsync(default);
await db.StockDailyPrice.AddAsync(StockDailyPrice.Create(stock.ID, date, close, close, close, close, 1000L, 1_000_000L, 0, changeRate));
await db.SaveChangesAsync(default);
return stock.ID;
}
[TestMethod]
public async Task Publish_CreatesSystemNoticePost_AndBriefingRecord()
{
var date = new DateOnly(2026, 7, 6);
using (var seed = TestDb.Create())
{
await SeedIndexAsync(seed, MarketIndexSeries.KOSPI, "코스피", new DateOnly(2026, 7, 3), 2800.55m, 12.30m, 44);
}
DailyBriefingComposer.PublishResult result;
using (var db = TestDb.Create())
{
result = await DailyBriefingComposer.PublishAsync(db, date, default);
}
Assert.IsTrue(result.Published, "최초 발행은 Published");
Assert.IsFalse(result.AlreadyExisted);
using var verify = TestDb.Create();
var briefing = await verify.DailyBriefing.AsNoTracking().SingleAsync(c => c.BriefingDate == date);
Assert.AreEqual(result.PostID, briefing.PostID);
var post = await verify.Post.AsNoTracking().SingleAsync(c => c.ID == result.PostID);
Assert.IsNull(post.MemberID, "시스템 계정 = MemberID null");
Assert.AreEqual(DailyBriefingComposer.SystemAuthorName, post.Name);
Assert.IsTrue(post.IsNotice, "브리핑은 공지로 발행");
StringAssert.Contains(post.Subject, "장전 시황");
// briefing 보드가 lazy 시드됐는지
var board = await verify.Board.AsNoTracking().SingleAsync(c => c.ID == post.BoardID);
Assert.AreEqual(BoardSeeder.BriefingBoardCode, board.Code);
}
[TestMethod]
public async Task Publish_Idempotent_SameDate_SkipsSecondRun()
{
var date = new DateOnly(2026, 7, 7);
using (var seed = TestDb.Create())
{
await SeedIndexAsync(seed, MarketIndexSeries.KOSDAQ, "코스닥", new DateOnly(2026, 7, 3), 850.10m, -3.20m, -37);
}
int firstPostID;
using (var db = TestDb.Create())
{
var r1 = await DailyBriefingComposer.PublishAsync(db, date, default);
Assert.IsTrue(r1.Published);
firstPostID = r1.PostID;
}
using (var db = TestDb.Create())
{
var r2 = await DailyBriefingComposer.PublishAsync(db, date, default);
Assert.IsFalse(r2.Published, "같은 날 재실행은 발행하지 않는다");
Assert.IsTrue(r2.AlreadyExisted);
Assert.AreEqual(firstPostID, r2.PostID, "기존 PostID 를 반환");
}
using var verify = TestDb.Create();
Assert.AreEqual(1, await verify.DailyBriefing.CountAsync(c => c.BriefingDate == date), "브리핑 기록은 1건뿐");
}
[TestMethod]
public async Task Publish_ComposesIndicesMoversAndDisclosures_IntoBody()
{
var date = new DateOnly(2026, 12, 31);
// 공유 DB 의 다른 테스트 시세보다 뒤 거래일이어야 movers 쿼리(글로벌 최신 거래일)가 이 종목을 잡는다.
var tradeDate = new DateOnly(2026, 12, 30);
var gainerCode = NextCode();
var loserCode = NextCode();
using (var db = TestDb.Create())
{
await SeedIndexAsync(db, MarketIndexSeries.KOSPI, "코스피", tradeDate, 2801.00m, 10.00m, 36);
await SeedStockWithPriceAsync(db, gainerCode, "급등주식회사", tradeDate, close: 15000, changeRate: 12.34m);
await SeedStockWithPriceAsync(db, loserCode, "급락주식회사", tradeDate, close: 8000, changeRate: -9.87m);
await db.Disclosure.AddAsync(Disclosure.Create("20260703000001", "00000001", "공시법인", gainerCode, "Y", "주요사항보고서", "공시법인", tradeDate));
await db.SaveChangesAsync(default);
}
int postID;
using (var db = TestDb.Create())
{
var r = await DailyBriefingComposer.PublishAsync(db, date, default);
postID = r.PostID;
}
using var verify = TestDb.Create();
var body = (await verify.Post.AsNoTracking().SingleAsync(c => c.ID == postID)).Content;
StringAssert.Contains(body, "전일 지수");
StringAssert.Contains(body, "코스피");
StringAssert.Contains(body, "등락 상위");
StringAssert.Contains(body, "급등주식회사");
StringAssert.Contains(body, "등락 하위");
StringAssert.Contains(body, "급락주식회사");
StringAssert.Contains(body, "주요 공시");
StringAssert.Contains(body, "주요사항보고서");
}
}