package study.graphql;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import org.springframework.test.web.reactive.server.WebTestClient;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * U50 schema/resolver、租户、错误与 N+1 的真实验证：
 * 用 spring-graphql 官方 HttpGraphQlTester 打真实 HTTP 端点（随机端口）。
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = {
                "spring.datasource.url=jdbc:h2:mem:u50;DB_CLOSE_DELAY=-1",
                "spring.sql.init.mode=never",
                "spring.graphql.graphiql.enabled=false",
                "spring.main.banner-mode=off"
        })
class GraphqlExecutionTest {

    @Autowired
    GraphqlLabApplication application;

    @Autowired
    LibraryRepository repository;

    @Autowired
    SqlCounter counter;

    @org.springframework.boot.test.web.server.LocalServerPort
    int port;

    HttpGraphQlTester tester;

    @BeforeEach
    void setUp() {
        repository.reset();
        WebTestClient client = WebTestClient.bindToServer()
                .baseUrl("http://localhost:" + port + "/graphql")
                .defaultHeaders(h -> h.setBasicAuth("alice", "pw"))
                .build();
        tester = HttpGraphQlTester.create(client);
        counter.reset();
    }

    @Test
    void schemaResolversAndTenantFilter() {
        // 1) 查询 books + 嵌套 author：真实 schema 绑定与字段解析
        tester.document("{ books(first: 10) { id title tenant author { name } } }")
                .execute()
                .path("books").entityList(Object.class).hasSize(3);

        // 2) 租户隔离：X-Tenant=tenant-b 只能看到 1 本 SECRET
        WebTestClient clientB = WebTestClient.bindToServer()
                .baseUrl("http://localhost:" + port + "/graphql")
                .defaultHeaders(h -> { h.setBasicAuth("alice", "pw"); h.set("X-Tenant", "tenant-b"); })
                .build();
        HttpGraphQlTester testerB = HttpGraphQlTester.create(clientB);
        testerB.document("{ books { title } }").execute()
                .path("books").entityList(Object.class).hasSize(1);
        System.out.println("U50 schemaResolvers=PASS tenantFilter=true");
    }

    @Test
    void nPlusOneVersusBatchMapping() {
        // N+1：books(3) 每条都解析 author → 1（books）+ 3（每本一次 author）= 4 条 SQL
        counter.reset();
        tester.document("{ books { id author { name } } }").execute()
                .path("books").entityList(Object.class).hasSize(3);
        int nPlusOne = counter.current();

        // 批量：authorBatch 用 @BatchMapping，3 本书一次 IN 查询 → 1 + 1 = 2 条 SQL
        counter.reset();
        tester.document("{ books { id authorBatch { name } } }").execute()
                .path("books").entityList(Object.class).hasSize(3);
        int batched = counter.current();

        System.out.println("U50 nPlusOne=PASS perBookSql=" + nPlusOne + " batchedSql=" + batched);
        assertTrue(nPlusOne > batched, "N+1 应比批量多 SQL：perBook=" + nPlusOne + " batched=" + batched);
    }

    @Test
    void validationAndErrorsInBody() {
        // @Valid 失败：BAD_REQUEST 错误进入响应 body（HTTP 仍 200）
        tester.document("mutation { addBook(input: {title: \"\", authorId: \"1\"}) { id title } }")
                .execute()
                .errors()
                .expect(error -> String.valueOf(error.getMessage()).contains("输入不合法"));
        System.out.println("U50 validationError=PASS httpStatusStill200=true");
    }

    @Test
    void mutationRequiresAdmin() {
        // alice 只有 READER：@PreAuthorize 拒绝 → FORBIDDEN 错误
        tester.document("mutation { addBook(input: {title: \"OK\", authorId: \"1\"}) { id title } }")
                .execute()
                .errors()
                .expect(error -> error.getMessage().contains("无权访问"));

        // admin 可以写：验证同一端点按身份区分
        WebTestClient adminClient = WebTestClient.bindToServer()
                .baseUrl("http://localhost:" + port + "/graphql")
                .defaultHeaders(h -> h.setBasicAuth("admin", "pw"))
                .build();
        HttpGraphQlTester admin = HttpGraphQlTester.create(adminClient);
        admin.document("mutation { addBook(input: {title: \"NEW\", authorId: \"1\"}) { id title } }")
                .execute()
                .path("addBook.title").entity(String.class).isEqualTo("NEW");
        System.out.println("U50 mutationAuth=PASS readerDenied=true adminAllowed=true");
    }

    @Test
    void dataLoaderDedupesKeysWithinRequest() {
        // 3 本书共享 2 个作者（A1、A2）：BatchLoaderRegistry 的 DataLoader 按 key 去重，
        // 批函数收到 2 个 key、只调用 1 次；与 @SchemaMapping 每书一查形成对照
        BatchLoaderConfig.reset();
        counter.reset();
        tester.document("{ books { id authorViaLoader { name } } }").execute()
                .path("books").entityList(Object.class).hasSize(3);
        int sql = counter.current();
        System.out.println("U50 dataLoaderDedupe=PASS batchCalls=" + BatchLoaderConfig.BATCH_CALLS.get()
                + " batchKeys=" + BatchLoaderConfig.BATCH_KEYS.get() + " sql=" + sql);
        assertTrue(BatchLoaderConfig.BATCH_CALLS.get() == 1, "批函数应只调用一次");
        assertTrue(BatchLoaderConfig.BATCH_KEYS.get() == 2, "5 个 load 调用应去重成 2 个 key");
        assertTrue(sql <= 2, "SQL 应为 books + 一次批量 authors：" + sql);
    }

    @Test
    void graphqlLevelValidationAndUnknownField() {
        // 语法/字段校验发生在执行前（GraphQL Java 校验规则），响应仍 200、errors 在 body
        tester.document("{ books { bogusField } }")
                .execute()
                .errors()
                .expect(error -> String.valueOf(error.getMessage()).contains("bogusField"));
        System.out.println("U50 graphqlValidation=PASS unknownFieldRejected=true http200=true");

        // 未处理异常（NumberFormatException）：不会被自定义 resolver 吞掉，落 INTERNAL 错误
        WebTestClient adminClient = WebTestClient.bindToServer()
                .baseUrl("http://localhost:" + port + "/graphql")
                .defaultHeaders(h -> h.setBasicAuth("admin", "pw"))
                .build();
        HttpGraphQlTester admin = HttpGraphQlTester.create(adminClient);
        admin.document("mutation { addBook(input: {title: \"BAD\", authorId: \"not-a-number\"}) { id } }")
                .execute()
                .errors()
                .satisfy(errors -> {
                    // 真实观察：未处理异常被服务端转成不透明的 INTERNAL_ERROR（不泄漏异常细节），
                    // 而不是把 NumberFormatException 原文返回给客户端
                    assertTrue(errors.stream().anyMatch(e ->
                                    String.valueOf(e.getMessage()).contains("INTERNAL_ERROR")),
                            "未处理异常应转成 INTERNAL_ERROR：" + errors);
                });
        System.out.println("U50 unhandledException=PASS fallsToDataFetchingError=true");
    }

    @Test
    void authRequiredAtHttpLayer() {
        // 无 HTTP 凭据：GraphQL over HTTP 被 Security 拦在端点外（401，不是 body 内 error）
        WebTestClient anonymous = WebTestClient.bindToServer()
                .baseUrl("http://localhost:" + port + "/graphql")
                .build();
        anonymous.post().uri("").header("Content-Type", "application/json")
                .bodyValue("{\"query\":\"{ books { title } }\"}")
                .exchange()
                .expectStatus().isUnauthorized();
        System.out.println("U50 httpAuth=PASS anonymous=401 bodyErrorsNotReached=true");
    }
}
