import Foundation
import TestSupport
import Testing
import WireProtocol
@testable import WebTerm

/// T-iOS-27 · Diff 查看器 —— `DiffFetcher`（App 层，GET /projects/diff）。
///
/// 覆盖面（Steps 测试先行）：
/// - 请求形状：GET + path/staged 严格百分号编码 + **无 Origin**（RO 端点，
///   §3.4 iff-G 铁律；服务器路由无 requireAllowedOrigin，src/server.ts:601-618）；
/// - `staged` 序列化为 `1`/`0` —— 服务器精确匹配 `=== '1'`（src/server.ts:613）；
/// - `DiffResult{files,staged,truncated}` 宽容解码（镜像 public/diff.ts
///   normalizeDiffResult：顶层三键缺一 → 整体无效；畸形 file/hunk/line 逐条
///   丢弃；未知 kind → .context、未知 status → .modified——服务器是不可信输入源）；
/// - 400 → .pathInvalid、404 → .projectNotFound（SEC-H7 三叉校验失败）、
///   其余状态 → .unexpectedStatus；
/// - 空 path 客户端先拒（镜像服务器 400 规则），零网络。
struct DiffFetcherTests {
    // MARK: - Fixtures

    private static func makeEndpoint() throws -> HostEndpoint {
        let url = try #require(URL(string: "http://127.0.0.1:3000"))
        return try #require(HostEndpoint(baseURL: url))
    }

    private static func makeFetcher() throws -> (DiffFetcher, FakeHTTPTransport) {
        let fake = FakeHTTPTransport()
        let fetcher = DiffFetcher(endpoint: try makeEndpoint(), http: fake)
        return (fetcher, fake)
    }

    private static func url(_ s: String) throws -> URL {
        try #require(URL(string: s))
    }

    /// 一份完整合法响应体（modified 文件 + 1 hunk + 4 行，覆盖 4 种 kind）。
    private static let fullBody = Data("""
    {
      "files": [
        {
          "oldPath": "src/a.ts", "newPath": "src/a.ts", "status": "modified",
          "added": 2, "removed": 1, "binary": false,
          "hunks": [
            { "header": "@@ -1,3 +1,4 @@",
              "lines": [
                { "kind": "context", "text": "unchanged" },
                { "kind": "removed", "text": "old line" },
                { "kind": "added", "text": "new line" },
                { "kind": "meta", "text": "No newline at end of file" }
              ] }
          ]
        }
      ],
      "staged": false,
      "truncated": false
    }
    """.utf8)

    // MARK: - 请求形状

    @Test("GET /projects/diff?path=<enc>&staged=0，无 Origin（RO 端点，iff-G 铁律）")
    func requestShapeIsReadOnlyGetWithoutOrigin() async throws {
        let (fetcher, fake) = try Self.makeFetcher()
        let expected = try Self.url(
            "http://127.0.0.1:3000/projects/diff?path=%2Ftmp%2Frepo&staged=0"
        )
        await fake.queueSuccess(url: expected, body: Self.fullBody)

        _ = try await fetcher.fetch(path: "/tmp/repo", staged: false)

        let recorded = await fake.recordedRequests
        let request = try #require(recorded.first)
        #expect(recorded.count == 1)
        #expect(request.httpMethod == "GET")
        #expect(request.url == expected)
        // RO 一律不带 Origin：服务器若把该端点改为 G，这里先红（§3.4 铁律）。
        #expect(request.value(forHTTPHeaderField: "Origin") == nil)
    }

    @Test("staged=true → staged=1（服务器 === '1' 精确匹配，src/server.ts:613）")
    func stagedSerializesAsOne() async throws {
        let (fetcher, fake) = try Self.makeFetcher()
        let expected = try Self.url(
            "http://127.0.0.1:3000/projects/diff?path=%2Ftmp%2Frepo&staged=1"
        )
        await fake.queueSuccess(url: expected, body: Self.fullBody)

        _ = try await fetcher.fetch(path: "/tmp/repo", staged: true)

        let recorded = await fake.recordedRequests
        #expect(recorded.first?.url == expected)
    }

    @Test("path 严格百分号编码：空格/&/+/= 全部转义（unreserved-only 集）")
    func pathIsStrictlyPercentEncoded() async throws {
        let (fetcher, fake) = try Self.makeFetcher()
        // " " → %20、"&" → %26、"+" → %2B、"=" → %3D、"/" → %2F
        let expected = try Self.url(
            "http://127.0.0.1:3000/projects/diff?path=%2Ftmp%2Fa%20b%26c%2Bd%3De&staged=0"
        )
        await fake.queueSuccess(url: expected, body: Self.fullBody)

        _ = try await fetcher.fetch(path: "/tmp/a b&c+d=e", staged: false)

        let recorded = await fake.recordedRequests
        #expect(recorded.first?.url == expected)
    }

    @Test("空 path → .invalidRequest，零网络（镜像服务器 400 规则，先于 I/O 拒绝）")
    func emptyPathRejectedBeforeNetwork() async throws {
        let (fetcher, fake) = try Self.makeFetcher()

        await #expect(throws: DiffFetchError.invalidRequest) {
            _ = try await fetcher.fetch(path: "", staged: false)
        }

        let recorded = await fake.recordedRequests
        #expect(recorded.isEmpty)
    }

    // MARK: - 解码（服务器为不可信输入源）

    @Test("200 完整体 → DiffResult 各字段逐一到位")
    func decodesFullBody() async throws {
        let (fetcher, fake) = try Self.makeFetcher()
        let expected = try Self.url(
            "http://127.0.0.1:3000/projects/diff?path=%2Ftmp%2Frepo&staged=0"
        )
        await fake.queueSuccess(url: expected, body: Self.fullBody)

        let result = try await fetcher.fetch(path: "/tmp/repo", staged: false)

        #expect(result.staged == false)
        #expect(result.truncated == false)
        #expect(result.files.count == 1)
        let file = try #require(result.files.first)
        #expect(file.newPath == "src/a.ts")
        #expect(file.status == .modified)
        #expect(file.added == 2)
        #expect(file.removed == 1)
        #expect(file.binary == false)
        let hunk = try #require(file.hunks.first)
        #expect(hunk.header == "@@ -1,3 +1,4 @@")
        #expect(hunk.lines.map(\.kind) == [.context, .removed, .added, .meta])
        #expect(hunk.lines.map(\.text) == [
            "unchanged", "old line", "new line", "No newline at end of file",
        ])
    }

    @Test("宽容解码：畸形 file 条目丢弃、未知 kind → .context、未知 status → .modified")
    func tolerantDecodingDropsMalformedEntries() async throws {
        let (fetcher, fake) = try Self.makeFetcher()
        let body = Data("""
        {
          "files": [
            42,
            { "oldPath": 1, "newPath": "x" },
            {
              "oldPath": "u.txt", "newPath": "u.txt", "status": "exotic-future",
              "added": 0, "removed": 0, "binary": false,
              "hunks": [
                { "lines": [] },
                { "header": "@@ -1 +1 @@",
                  "lines": [
                    { "kind": "sparkle", "text": "mystery" },
                    { "kind": "added" },
                    { "kind": "added", "text": "kept" }
                  ] }
              ]
            }
          ],
          "staged": true,
          "truncated": true
        }
        """.utf8)
        let expected = try Self.url(
            "http://127.0.0.1:3000/projects/diff?path=%2Ftmp%2Frepo&staged=1"
        )
        await fake.queueSuccess(url: expected, body: body)

        let result = try await fetcher.fetch(path: "/tmp/repo", staged: true)

        #expect(result.truncated == true)
        #expect(result.files.count == 1) // 42 与缺字段对象被丢弃
        let file = try #require(result.files.first)
        #expect(file.status == .modified)      // 未知 status 降级
        #expect(file.hunks.count == 1)         // 缺 header 的 hunk 被丢弃
        let lines = try #require(file.hunks.first).lines
        #expect(lines.count == 2)              // 缺 text 的行被丢弃
        #expect(lines.first?.kind == .context) // 未知 kind 降级（同 web df-context 兜底）
        #expect(lines.last?.text == "kept")
    }

    @Test("顶层畸形（缺键 / 非对象 / 非 JSON）→ .invalidResponse", arguments: [
        #"{"staged": false, "truncated": false}"#,
        #"[1, 2, 3]"#,
        "not json at all",
    ])
    func malformedTopLevelIsInvalidResponse(raw: String) async throws {
        let (fetcher, fake) = try Self.makeFetcher()
        let expected = try Self.url(
            "http://127.0.0.1:3000/projects/diff?path=%2Ftmp%2Frepo&staged=0"
        )
        await fake.queueSuccess(url: expected, body: Data(raw.utf8))

        await #expect(throws: DiffFetchError.invalidResponse) {
            _ = try await fetcher.fetch(path: "/tmp/repo", staged: false)
        }
    }

    // MARK: - 错误状态映射（route 层语义，src/server.ts:602-618）

    @Test("400 → .pathInvalid、404 → .projectNotFound、500 → .unexpectedStatus(500)")
    func statusCodesMapToTypedErrors() async throws {
        let cases: [(Int, DiffFetchError)] = [
            (400, .pathInvalid),
            (404, .projectNotFound),
            (500, .unexpectedStatus(500)),
            (403, .unexpectedStatus(403)),
        ]
        for (status, expectedError) in cases {
            let (fetcher, fake) = try Self.makeFetcher()
            let expected = try Self.url(
                "http://127.0.0.1:3000/projects/diff?path=%2Ftmp%2Frepo&staged=0"
            )
            await fake.queueSuccess(
                url: expected, status: status, body: Data(#"{"error":"x"}"#.utf8)
            )

            await #expect(throws: expectedError) {
                _ = try await fetcher.fetch(path: "/tmp/repo", staged: false)
            }
        }
    }

    @Test("传输层错误原样上抛（不吞、不错误分类）")
    func transportErrorsPropagate() async throws {
        struct Boom: Error {}
        let (fetcher, fake) = try Self.makeFetcher()
        let expected = try Self.url(
            "http://127.0.0.1:3000/projects/diff?path=%2Ftmp%2Frepo&staged=0"
        )
        await fake.queueFailure(url: expected, error: Boom())

        await #expect(throws: Boom.self) {
            _ = try await fetcher.fetch(path: "/tmp/repo", staged: false)
        }
    }
}
