package social.grain.support import social.grain.data.model.AspectRatio import social.grain.data.model.AtLabel import social.grain.data.model.GalleryViewerState import social.grain.data.model.GrainComment import social.grain.data.model.GrainGallery import social.grain.data.model.GrainNotification import social.grain.data.model.GrainPhoto import social.grain.data.model.GrainProfile import social.grain.data.model.GrainProfileDetailed import social.grain.data.model.GrainStory import social.grain.data.model.GrainStoryAuthor import social.grain.data.model.H3Location import social.grain.util.nowIso import java.time.Instant /** * Hand-built models for tests that need something to render or reason over. * * Deliberately not read off the network like the older decode tests: those * exist to catch the server changing shape, and these exist to pin behaviour * down to one input, which a live feed can't do. */ object Fixtures { fun instant(minutesAgo: Long = 0): Instant = Instant.parse("2026-03-01T12:00:00Z").minusSeconds(minutesAgo * 60) fun iso(minutesAgo: Long = 0): String = nowIso(instant(minutesAgo)) /** Recent enough that anything with a 24-hour window still counts it. */ fun recentIso(minutesAgo: Long = 5): String = nowIso(Instant.now().minusSeconds(minutesAgo * 60)) fun profile( did: String = "did:plc:alice", handle: String = "alice.grain.social", displayName: String? = "Alice", avatar: String? = "https://cdn.grain.test/avatar/alice.jpg", ) = GrainProfile( cid = "cid-$did", did = did, handle = handle, displayName = displayName, avatar = avatar, createdAt = iso(60 * 24 * 30), ) fun profileDetailed( did: String = "did:plc:alice", handle: String = "alice.grain.social", displayName: String? = "Alice", following: String? = null, followedBy: String? = null, blocking: String? = null, muted: Boolean? = null, labels: List? = null, ) = GrainProfileDetailed( cid = "cid-$did", did = did, handle = handle, displayName = displayName, description = "Photographs of things", avatar = "https://cdn.grain.test/avatar/$handle.jpg", cameras = listOf("Fujifilm X100V"), followersCount = 120, followsCount = 80, galleryCount = 12, createdAt = iso(60 * 24 * 30), viewer = social.grain.data.model.ActorViewerState( following = following, followedBy = followedBy, blocking = blocking, muted = muted, ), labels = labels, ) fun photo( index: Int = 0, width: Int = 3, height: Int = 2, alt: String? = null, ) = GrainPhoto( uri = "at://did:plc:alice/social.grain.photo/photo$index", cid = "cid-photo$index", thumb = "https://cdn.grain.test/thumb/$index.jpg", fullsize = "https://cdn.grain.test/full/$index.jpg", alt = alt, aspectRatio = AspectRatio(width, height), ) fun gallery( rkey: String = "gallery1", title: String? = "A walk in the fog", description: String? = "Six frames from a grey morning", creator: GrainProfile = profile(), photos: Int = 3, favCount: Int? = 12, commentCount: Int? = 3, labels: List? = null, favUri: String? = null, location: H3Location? = null, ) = GrainGallery( uri = "at://${creator.did}/social.grain.gallery/$rkey", cid = "cid-$rkey", title = title, description = description, location = location, locationDisplay = location?.name, creator = creator, items = List(photos) { photo(it) }, favCount = favCount, commentCount = commentCount, labels = labels, createdAt = iso(30), indexedAt = iso(30), viewer = GalleryViewerState(fav = favUri), ) fun comment( rkey: String = "comment1", text: String = "Lovely light", author: GrainProfile = profile(), replyTo: String? = null, favUri: String? = null, minutesAgo: Long = 10, ) = GrainComment( uri = "at://${author.did}/social.grain.comment/$rkey", cid = "cid-$rkey", author = author, text = text, replyTo = replyTo, createdAt = iso(minutesAgo), favCount = 0, viewer = social.grain.data.model.CommentViewerState(fav = favUri), ) fun story( rkey: String = "story1", creator: GrainProfile = profile(), createdAt: String = recentIso(), expired: Boolean? = null, favUri: String? = null, labels: List? = null, ) = GrainStory( uri = "at://${creator.did}/social.grain.story/$rkey", cid = "cid-$rkey", creator = creator, thumb = "https://cdn.grain.test/story-thumb/$rkey.jpg", fullsize = "https://cdn.grain.test/story/$rkey.jpg", aspectRatio = AspectRatio(9, 16), createdAt = createdAt, labels = labels, expired = expired, viewer = social.grain.data.model.StoryViewerState(fav = favUri), ) fun storyAuthor( profile: GrainProfile = profile(), storyCount: Int = 2, latestAt: String = recentIso(), ) = GrainStoryAuthor(profile = profile, storyCount = storyCount, latestAt = latestAt) fun notification( rkey: String = "notif1", reason: String = "gallery-favorite", author: GrainProfile = profile(), galleryUri: String? = "at://did:plc:alice/social.grain.gallery/gallery1", minutesAgo: Long = 5, commentText: String? = null, ) = GrainNotification( uri = "at://${author.did}/notification/$rkey", reason = reason, createdAt = iso(minutesAgo), author = author, galleryUri = galleryUri, galleryTitle = "A walk in the fog", galleryThumb = "https://cdn.grain.test/thumb/0.jpg", commentText = commentText, ) } /** Encode a fixture to the JSON the stub server hands back. */ inline fun json(value: T): String = testJson.encodeToString(value)