import XCTest
@testable import DSKit

/// Offline tests for the live-client plumbing (the live HTTP path itself needs a real backend, FU-A).
final class ClientPlumbingTests: XCTestCase {

    func testEndpointPathsAndMethods() {
        XCTAssertEqual(DSEndpoint.documents(DocumentListQuery()).path, "documents/")
        XCTAssertEqual(DSEndpoint.documents(DocumentListQuery()).method, "GET")
        XCTAssertEqual(DSEndpoint.document(4912).path, "documents/4912")
        XCTAssertEqual(DSEndpoint.documentTree.path, "documents/tree")
        XCTAssertEqual(DSEndpoint.ask("q", nil, nil, nil).path, "search/ask")
        XCTAssertEqual(DSEndpoint.pinMemo(5, true).method, "PATCH")
        XCTAssertEqual(DSEndpoint.pinMemo(5, true).path, "memos/5/pin")
        XCTAssertEqual(DSEndpoint.toggleMemoTask(5, 2, true).path, "memos/5/tasks/2")
        XCTAssertEqual(DSEndpoint.putContent(7, "x").method, "PUT")
        XCTAssertEqual(DSEndpoint.deleteMemo(9).method, "DELETE")
    }

    func testEndpointBearerRule() {
        XCTAssertFalse(DSEndpoint.login("u", "p", nil).requiresBearer)
        XCTAssertFalse(DSEndpoint.refresh.requiresBearer)
        XCTAssertTrue(DSEndpoint.me.requiresBearer)
        XCTAssertTrue(DSEndpoint.documents(DocumentListQuery()).requiresBearer)
    }

    func testEndpointQueryItems() {
        let items = DSEndpoint.ask("충격시험", 5, "gemma-macmini", nil).queryItems
        let dict = Dictionary(uniqueKeysWithValues: items.map { ($0.name, $0.value) })
        XCTAssertEqual(dict["q"], "충격시험")
        XCTAssertEqual(dict["backend"], "gemma-macmini")
        XCTAssertEqual(dict["limit"], "5")
        XCTAssertNil(dict["debug"]) // nil params are skipped, not sent as "nil"
    }

    func testEndpointBodies() throws {
        let encoder = DSEncoder.make()
        let pinBody = try DSEndpoint.pinMemo(5, true).httpBody(encoder)
        XCTAssertEqual(String(data: pinBody ?? Data(), encoding: .utf8), #"{"pinned":true}"#)
        let loginBody = try XCTUnwrap(DSEndpoint.login("hyungi", "pw", nil).httpBody(encoder))
        let s = String(data: loginBody, encoding: .utf8) ?? ""
        XCTAssertTrue(s.contains("\"username\":\"hyungi\""))
        XCTAssertFalse(s.contains("totp_code")) // nil optional omitted
        XCTAssertNil(try DSEndpoint.me.httpBody(encoder)) // GET has no body
    }

    /// Single-flight refresh: N concurrent refreshOnce() calls must fire the refresh closure exactly once.
    func testSingleFlightRefresh() async throws {
        let counter = CallCounter()
        let provider = TokenProvider(persistence: InMemoryTokenStore()) {
            try? await Task.sleep(nanoseconds: 50_000_000)
            let n = await counter.inc()
            return "token-\(n)"
        }
        let results = await withTaskGroup(of: String.self) { group -> [String] in
            for _ in 0..<8 { group.addTask { (try? await provider.refreshOnce()) ?? "ERR" } }
            var collected: [String] = []
            for await value in group { collected.append(value) }
            return collected
        }
        let calls = await counter.value
        XCTAssertEqual(calls, 1, "refresh closure must run exactly once under concurrency")
        XCTAssertTrue(results.allSatisfy { $0 == "token-1" })
    }

    func testTokenProviderCacheAndPersistence() async {
        let store = InMemoryTokenStore()
        let provider = TokenProvider(persistence: store) { "x" }
        await provider.set("abc")
        let current = await provider.current()
        XCTAssertEqual(current, "abc")
        XCTAssertEqual(store.read(), "abc")
        await provider.clear()
        let cleared = await provider.current()
        XCTAssertNil(cleared)
    }
}

actor CallCounter {
    var value = 0
    func inc() -> Int { value += 1; return value }
}
