@file:Suppress("unused", "ClassName")

package viaduct.tenant.tutorial04

import org.junit.jupiter.api.Test
import viaduct.api.Resolver
import viaduct.graphql.test.assertEquals
import viaduct.tenant.runtime.fixtures.FeatureAppTestBase
import viaduct.tenant.tutorial04.resolverbases.QueryResolvers
import viaduct.tenant.tutorial04.resolverbases.UserResolvers

/**
 * LEARNING OBJECTIVES:
 * - Eliminate redundant expensive operations across multiple fields
 * - Share complex Kotlin objects between field resolvers
 * - Understand @backingData directive and BackingData type
 * - Learn efficient patterns for external service integration
 *
 * VIADUCT FEATURES DEMONSTRATED:
 * - @backingData directive with custom class specification
 * - BackingData type in schema
 * - ctx.objectValue.get() for type-safe backing data access
 * - Single expensive operation powering multiple GraphQL fields
 *
 * CONCEPTS COVERED:
 * - Backing data as internal optimization (not exposed to GraphQL)
 * - Custom Kotlin data classes for complex internal state
 * - Service layer integration patterns
 * - Performance optimization through data sharing
 *
 * PREVIOUS: [viaduct.tenant.tutorial03.SimpleResolversFeatureAppTest]
 * NEXT: [viaduct.tenant.tutorial05.SimpleMutationsFeatureAppTest]
 */
class SimpleBackingDataFeatureAppTest : FeatureAppTestBase() {
    override var sdl = """
        | #START_SCHEMA
        | extend type Query {
        |   user(id: String!): User! @resolver
        | }
        |
        | type User implements Node @resolver {
        |   id: ID!
        |   name: String!
        |   email: String!
        |   averageStars: Float! @resolver      # Computed from backing data
        |   reviewsCount: Int! @resolver        # Computed from backing data
        |   reviewsData: BackingData            # The expensive operation
        |     @resolver
        |     @backingData(class: "UserReviewsData")  # Your custom Kotlin class
        | }
        | #END_SCHEMA
    """.trimMargin()

    /**
     * NODE RESOLVER - Handles basic User data
     *
     * What YOU write:
     * - Fetch basic entity data (typically from primary database)
     * - Return User objects with core fields
     *
     * What VIADUCT generates:
     * - NodeResolvers.User() base class
     * - User.Builder for construction
     */
    @Resolver
    class UserNodeResolver : NodeResolvers.User() {
        override suspend fun resolve(ctx: Context): User {
            // YOUR BUSINESS LOGIC - typically database lookup
            // In production: userRepository.findById(internalId)

            // TEST SIMULATION - Mock user data
            val userData = when (val internalId = ctx.id.internalID) {
                "user-123" -> Pair("John Smith", "john@example.com")
                "user-456" -> Pair("Jane Doe", "jane@example.com")
                else -> throw IllegalArgumentException("User not found: $internalId")
            }

            return User.Builder(ctx)
                .id(ctx.id)
                .name(userData.first)
                .email(userData.second)
                .build()
        }
    }

    @Resolver
    class UserQueryResolver : QueryResolvers.User() {
        override suspend fun resolve(ctx: Context): User {
            return ctx.nodeFor(ctx.globalIDFor(User.Reflection, ctx.arguments.id))
        }
    }

    /**
     * BACKING DATA RESOLVER - The expensive operation
     *
     * What YOU write:
     * - Return your custom Kotlin object (not generated by Viaduct)
     * - Implement expensive operation once (API call, complex computation, etc.)
     * - Use objectValueFragment to access parent data needed for the operation
     *
     * What VIADUCT handles:
     * - Runs this resolver only once per object, regardless of how many fields need the data
     * - Caches result for use by multiple field resolvers
     * - Never serializes this data to GraphQL (stays internal)
     */
    @Resolver(
        """fragment _ on User { id }"""
    )
    class UserReviewsDataResolver : UserResolvers.ReviewsData() {
        override suspend fun resolve(ctx: Context): UserReviewsData {
            val userId = ctx.objectValue.getId().internalID

            // EXPENSIVE OPERATION - simulates external service call
            // In production: reviewsService.getUserReviewSummary(userId)
            // Or: complexAnalytics.computeUserMetrics(userId)
            println("EXPENSIVE OPERATION: Fetching reviews data for $userId")

            // TEST SIMULATION - Mock expensive external service call
            return when (userId) {
                "user-123" -> MOCK_USER_123_REVIEWS
                "user-456" -> MOCK_USER_456_REVIEWS
                else -> UserReviewsData(averageRating = 0.0, totalReviews = 0)
            }
        }
    }

    /**
     * FIELD RESOLVER 1 - Extracts averageStars from shared backing data
     *
     * Key pattern: Uses ctx.objectValue.get() to access the UserReviewsData
     * that was computed by UserReviewsDataResolver
     */
    @Resolver(
        """
        fragment _ on User { reviewsData }
        """
    )
    class UserAverageStarsResolver : UserResolvers.AverageStars() {
        override suspend fun resolve(ctx: Context): Double {
            // TYPE-SAFE BACKING DATA ACCESS
            val reviewsData = ctx.objectValue.get<UserReviewsData>("reviewsData", UserReviewsData::class)
            return reviewsData.averageRating

            // No expensive operation here - just data extraction!
        }
    }

    /**
     * FIELD RESOLVER 2 - Extracts reviewsCount from same backing data
     *
     * Both this and averageStars share the SAME UserReviewsData instance
     */
    @Resolver(
        """fragment _ on User { reviewsData }"""
    )
    class UserReviewsCountResolver : UserResolvers.ReviewsCount() {
        override suspend fun resolve(ctx: Context): Int {
            // REUSES THE SAME BACKING DATA - no additional expensive operation
            val reviewsData = ctx.objectValue.get<UserReviewsData>("reviewsData", UserReviewsData::class)
            return reviewsData.totalReviews
        }
    }

    @Test
    fun `Backing data enables multiple fields to share expensive reviews service call`() {
        execute(
            query = """
                query {
                    user(id: "user-123") {
                        name
                        email
                        averageStars    # Triggers UserReviewsDataResolver ONCE
                        reviewsCount    # Reuses same UserReviewsData instance
                    }
                }
            """.trimIndent()
        ).assertEquals {
            "data" to {
                "user" to {
                    "name" to "John Smith"
                    "email" to "john@example.com"
                    "averageStars" to MOCK_USER_123_REVIEWS.averageRating
                    "reviewsCount" to MOCK_USER_123_REVIEWS.totalReviews
                }
            }
        }
    }

    @Test
    fun `Individual review fields can be queried independently`() {
        execute(
            query = """
                query {
                    user(id: "user-456") {
                        averageStars
                    }
                }
            """.trimIndent()
        ).assertEquals {
            "data" to {
                "user" to {
                    "averageStars" to MOCK_USER_456_REVIEWS.averageRating
                }
            }
        }
    }

    @Test
    fun `Main database fields work independently of reviews data`() {
        execute(
            query = """
                query {
                    user(id: "user-123") {
                        name
                        email
                    }
                }
            """.trimIndent()
        ).assertEquals {
            "data" to {
                "user" to {
                    "name" to "John Smith"
                    "email" to "john@example.com"
                }
            }
        }
    }

    companion object {
        // TEST DATA - simulates external service response
        val MOCK_USER_123_REVIEWS = UserReviewsData(
            averageRating = 4.2,
            totalReviews = 127,
        )

        val MOCK_USER_456_REVIEWS = UserReviewsData(
            averageRating = 3.8,
            totalReviews = 43,
        )
    }

    /**
     * EXECUTION FLOW WITH BACKING DATA:
     *
     * Query: user(id: "user-123") { averageStars, reviewsCount }
     *
     * 1. UserQueryResolver -> UserNodeResolver (creates basic User object)
     * 2. Viaduct sees averageStars + reviewsCount both need reviewsData
     * 3. UserReviewsDataResolver.resolve() called ONCE
     * 4. UserReviewsData(4.2, 127) stored as backing data
     * 5. UserAverageStarsResolver accesses backing data -> returns 4.2
     * 6. UserReviewsCountResolver accesses SAME backing data -> returns 127
     *
     * KEY TAKEAWAYS:
     * - Backing data stays internal (never serialized to GraphQL)
     * - One expensive operation powers multiple GraphQL fields
     * - Use custom Kotlin classes for complex internal state
     * - Perfect for external service integration
     * - Alternative to batch resolvers for same-object scenarios
     */
}

// YOUR CUSTOM KOTLIN CLASS - not generated by Viaduct
data class UserReviewsData(
    val averageRating: Double,
    val totalReviews: Int,
)
