import Foundation
import Testing
import TestSupport
import WireProtocol
import APIClient

/// T-iOS-38 · `GET /prefs`(RO,无 Origin) / `PUT /prefs`(G,Origin;body ≤ 64 KB,
/// src/server.ts:278)。prefs 是"不透明但已验证"的 JSON 对象往返：已知键
/// favourites/collapsed 松散镜像 web 端（public/prefs.ts sanitizePrefs），
/// **未知键在往返中原样保留** —— 关键正确性陷阱：iOS 改一个已知键后 PUT，
/// 绝不能把 web/未来服务器写入的未知键清掉。
struct PrefsRoundTripTests {
    private static let base = "http://192.168.1.5:3000"

    /// 含未知键的服务器 blob：string/number/bool/null/嵌套 array/object 全类型。
    private static let blobWithUnknownKeys = """
    {"favourites":["/Users/dev/a"],"collapsed":{"grp":true},\
    "theme":"dark","pollSeconds":42,"beta":false,"legacy":null,\
    "futureList":[1,"two",{"deep":true}],"futureObj":{"x":{"y":[3]}}}
    """

    private struct Fixture {
        let endpoint: HostEndpoint
        let http: FakeHTTPTransport
        let client: APIClient
        let url: URL
    }

    private func makeFixture() throws -> Fixture {
        let baseURL = try #require(URL(string: Self.base))
        let endpoint = try #require(HostEndpoint(baseURL: baseURL))
        let http = FakeHTTPTransport()
        return Fixture(
            endpoint: endpoint,
            http: http,
            client: APIClient(endpoint: endpoint, http: http),
            url: try #require(URL(string: Self.base + "/prefs"))
        )
    }

    private func decodePrefs(_ json: String) throws -> UiPrefs {
        try #require(UiPrefs.decode(from: Data(json.utf8)))
    }

    /// 重新解析 encode 产物为字典（键序无关的深度比较用）。
    private func reparse(_ data: Data) throws -> NSDictionary {
        try #require(try JSONSerialization.jsonObject(with: data) as? NSDictionary)
    }

    // MARK: - Origin iff-G

    @Test("Origin iff-G:GET /prefs 无 Origin;PUT /prefs 带 Origin(逐字符相等)+JSON Content-Type")
    func prefsOriginAppearsIffPut() async throws {
        // Arrange
        let fixture = try makeFixture()
        await fixture.http.queueSuccess(url: fixture.url, body: Data(Self.blobWithUnknownKeys.utf8))
        await fixture.http.queueSuccess(
            method: "PUT", url: fixture.url, body: Data(#"{"favourites":[],"collapsed":{}}"#.utf8)
        )

        // Act
        _ = try await fixture.client.prefs()
        _ = try await fixture.client.putPrefs(UiPrefs())

        // Assert
        let requests = await fixture.http.recordedRequests
        #expect(requests.count == 2)
        let get = try #require(requests.first)
        #expect(get.httpMethod == "GET")
        #expect(get.value(forHTTPHeaderField: "Origin") == nil)
        let put = try #require(requests.last)
        #expect(put.httpMethod == "PUT")
        #expect(put.value(forHTTPHeaderField: "Origin") == fixture.endpoint.originHeader)
        #expect(put.value(forHTTPHeaderField: "Content-Type") == "application/json")
    }

    // MARK: - 已知键的松散镜像（web sanitizePrefs 语义）

    @Test("已知键解码:favourites 只留非空字符串并去重;collapsed 只留 true 项")
    func knownKeysAreSanitizedLikeWebClient() throws {
        // Arrange — 混入非字符串/空串/重复/非 true 值
        let json = """
        {"favourites":["/a",42,"","/a","/b"],\
        "collapsed":{"g1":true,"g2":false,"g3":"yes","":true}}
        """

        // Act
        let prefs = try decodePrefs(json)

        // Assert
        #expect(prefs.favourites == ["/a", "/b"])
        #expect(prefs.collapsed == ["g1": true])
    }

    @Test("已知键缺失/类型错 → 空缺省;空构造与 memberwise 构造可用")
    func missingKnownKeysDegradeToEmpty() throws {
        // Act
        let empty = try decodePrefs(#"{"favourites":"not-an-array"}"#)
        let constructed = UiPrefs(favourites: ["/x"], collapsed: ["g": true])

        // Assert
        #expect(empty.favourites.isEmpty)
        #expect(empty.collapsed.isEmpty)
        #expect(constructed.favourites == ["/x"])
        #expect(constructed.collapsed == ["g": true])
    }

    // MARK: - 关键正确性陷阱：未知键在往返中存活

    @Test("关键陷阱:解码含未知键的 blob → withFavourites 改已知键 → 重编码,未知键逐字节存活")
    func unknownKeysSurviveFavouritesMutationRoundTrip() throws {
        // Arrange
        let original = try decodePrefs(Self.blobWithUnknownKeys)

        // Act — 只动 favourites
        let mutated = original.withFavourites(["/Users/dev/a", "/Users/dev/b"])
        let reencoded = try reparse(try mutated.encodeBody())

        // Assert — 已知键更新、另一已知键不动、未知键全类型深度相等
        #expect(mutated.favourites == ["/Users/dev/a", "/Users/dev/b"])
        #expect(reencoded["favourites"] as? [String] == ["/Users/dev/a", "/Users/dev/b"])
        #expect(reencoded["collapsed"] as? [String: Bool] == ["grp": true])
        #expect(reencoded["theme"] as? String == "dark")
        #expect(reencoded["pollSeconds"] as? Int == 42)
        #expect(reencoded["beta"] as? Bool == false)
        #expect(reencoded["legacy"] is NSNull)
        #expect(reencoded["futureList"] as? NSArray == [1, "two", ["deep": true]] as NSArray)
        #expect(reencoded["futureObj"] as? NSDictionary == ["x": ["y": [3]]] as NSDictionary)
        // 原值不可变（plan §4 不可变铁律）
        #expect(original.favourites == ["/Users/dev/a"])
    }

    @Test("withCollapsed 同样保留未知键与 favourites;往返不放大不丢失")
    func unknownKeysSurviveCollapsedMutationRoundTrip() throws {
        // Arrange
        let original = try decodePrefs(Self.blobWithUnknownKeys)

        // Act
        let mutated = original.withCollapsed(["grp": true, "new-group": true])
        let reencoded = try reparse(try mutated.encodeBody())

        // Assert
        #expect(reencoded["collapsed"] as? [String: Bool] == ["grp": true, "new-group": true])
        #expect(reencoded["favourites"] as? [String] == ["/Users/dev/a"])
        #expect(reencoded["theme"] as? String == "dark")
        #expect(reencoded["futureObj"] as? NSDictionary == ["x": ["y": [3]]] as NSDictionary)
        // 未知键总数不变（8 个顶层键）
        #expect(reencoded.count == 8)
    }

    // MARK: - 客户端调用（echo/错误映射）

    @Test("putPrefs 200 → 返回服务器回显的(已净化)prefs;403(Origin 守卫) → forbidden")
    func putPrefsReturnsServerEchoAndMapsForbidden() async throws {
        // Arrange — 服务器回显是净化后的 blob(src/server.ts:283 res.json(prefsStore.get()))
        let fixture = try makeFixture()
        await fixture.http.queueSuccess(
            method: "PUT", url: fixture.url,
            body: Data(#"{"favourites":["/kept"],"collapsed":{}}"#.utf8)
        )
        await fixture.http.queueSuccess(method: "PUT", url: fixture.url, status: 403)

        // Act
        let echoed = try await fixture.client.putPrefs(UiPrefs(favourites: ["/kept", ""]))

        // Assert
        #expect(echoed.favourites == ["/kept"])
        await #expect(throws: APIClientError.forbidden) {
            _ = try await fixture.client.putPrefs(UiPrefs())
        }
    }

    @Test("GET /prefs 顶层非对象(数组/HTML) → invalidResponseBody(绝不静默降级为空——防止后续 PUT 清空服务器 prefs)")
    func nonObjectPrefsBodyFailsLoudlyInsteadOfDegradingToEmpty() async throws {
        // Arrange
        let fixture = try makeFixture()
        await fixture.http.queueSuccess(url: fixture.url, body: Data("[]".utf8))
        await fixture.http.queueSuccess(url: fixture.url, body: Data("<html>".utf8))

        // Act + Assert
        await #expect(throws: APIClientError.invalidResponseBody) {
            _ = try await fixture.client.prefs()
        }
        await #expect(throws: APIClientError.invalidResponseBody) {
            _ = try await fixture.client.prefs()
        }
    }
}
