import APIClient
import Foundation
import HostRegistry
import Testing
import TestSupport
import UserNotifications
import WireProtocol
@testable import WebTerm

/// T-iOS-21 · NotificationActionHandler.handle：Allow/Deny → POST
/// /hook/decision（背景任务包住、403 → 本地通知兜底）、默认点按 →
/// DeepLinkRouter 同源路由到会话（parse 的 payload 级用例在
/// NotificationActionParseTests.swift）。
/// 说明：UNNotificationResponse 无法在单测构造 —— didReceive 薄胶水不在
/// 单测覆盖内，全部逻辑经 parse(actionIdentifier:userInfo:) + handle(_:) 测。
@MainActor
@Suite("NotificationActionHandler")
struct NotificationActionHandlerTests {
    // MARK: - Fakes

    @MainActor
    private final class FakeBackgroundTasks: BackgroundTaskRunning {
        private(set) var begunNames: [String] = []
        private(set) var endedTokens: [Int] = []
        private var nextToken = 0

        func begin(name: String) -> Int {
            begunNames.append(name)
            nextToken += 1
            return nextToken
        }

        func end(_ token: Int) { endedTokens.append(token) }
    }

    @MainActor
    private final class FakeNoticePoster: LocalNoticePosting {
        private(set) var posted: [(title: String, body: String)] = []
        func post(title: String, body: String) async { posted.append((title, body)) }
    }

    @MainActor
    private final class OpenRecorder {
        private(set) var opened: [(host: HostRegistry.Host, sessionId: UUID)] = []
        var actions: NotificationActionHandler.Actions {
            NotificationActionHandler.Actions(openSession: { [weak self] host, sessionId in
                self?.opened.append((host, sessionId))
            })
        }
    }

    // MARK: - Fixtures

    private static let sessionId = "0f5a1f2e-3b4c-4d5e-8f6a-7b8c9d0e1f2a"
    /// 服务器 capability token = crypto.randomUUID()（src/server.ts:445）→ v4 形状。
    private static let token = "7c1de1e0-9a2b-4c3d-8e4f-5a6b7c8d9e0f"
    private static let hostABase = "http://192.168.1.5:3000"
    private static let hostBBase = "http://192.168.1.6:3000"

    private static func makeHost(base: String, name: String = "mac") throws -> HostRegistry.Host {
        let url = try #require(URL(string: base))
        let endpoint = try #require(HostEndpoint(baseURL: url))
        return HostRegistry.Host(id: UUID(), name: name, endpoint: endpoint)
    }

    private struct Harness {
        let handler: NotificationActionHandler
        let http: FakeHTTPTransport
        let backgroundTasks: FakeBackgroundTasks
        let notices: FakeNoticePoster
        let recorder: OpenRecorder
    }

    private static func makeHarness(hosts: [HostRegistry.Host]) -> Harness {
        let http = FakeHTTPTransport()
        let backgroundTasks = FakeBackgroundTasks()
        let notices = FakeNoticePoster()
        let recorder = OpenRecorder()
        let handler = NotificationActionHandler(
            hostStore: InMemoryHostStore(hosts: hosts),
            http: http,
            backgroundTasks: backgroundTasks,
            notices: notices,
            actions: recorder.actions
        )
        return Harness(
            handler: handler, http: http, backgroundTasks: backgroundTasks,
            notices: notices, recorder: recorder
        )
    }

    private static func decisionURL(base: String) throws -> URL {
        try #require(URL(string: "\(base)/hook/decision"))
    }

    private static func liveSessionsURL(base: String) throws -> URL {
        try #require(URL(string: "\(base)/live-sessions"))
    }

    /// 多主机场景的两条 RO 探查响应一次排好（A 无、B 持有目标会话）。
    private static func queueTwoHostProbe(_ harness: Harness) async throws {
        await harness.http.queueSuccess(
            method: "GET", url: try liveSessionsURL(base: hostABase),
            body: liveSessionsBody(containing: nil)
        )
        await harness.http.queueSuccess(
            method: "GET", url: try liveSessionsURL(base: hostBBase),
            body: liveSessionsBody(containing: sessionId)
        )
    }

    /// /live-sessions 响应体：可选包含目标会话（必填字段齐全的最小形状）。
    private static func liveSessionsBody(containing sessionId: String?) -> Data {
        guard let sessionId else { return Data("[]".utf8) }
        let json = """
        [{"id":"\(sessionId)","createdAt":1,"clientCount":0,"status":"waiting",
        "exited":false,"cols":80,"rows":24}]
        """
        return Data(json.utf8)
    }

    // MARK: - handle(.decision)：POST /hook/decision

    @Test("单主机 Allow → 直接 POST（免 RO 探查），体为 {sessionId,decision,token}，带 Origin，背景任务成对")
    func decisionSingleHostPosts() async throws {
        let host = try Self.makeHost(base: Self.hostABase)
        let harness = Self.makeHarness(hosts: [host])
        await harness.http.queueSuccess(
            method: "POST", url: try Self.decisionURL(base: Self.hostABase), status: 204
        )
        let sessionId = try #require(UUID(uuidString: Self.sessionId))

        await harness.handler.handle(
            .decision(.allow, sessionId: sessionId, token: Self.token)
        )

        let requests = await harness.http.recordedRequests
        #expect(requests.count == 1)
        let request = try #require(requests.first)
        #expect(request.httpMethod == "POST")
        #expect(request.value(forHTTPHeaderField: "Origin") == Self.hostABase)
        let body = try JSONDecoder().decode(
            [String: String].self, from: try #require(request.httpBody)
        )
        #expect(body == [
            "sessionId": Self.sessionId, "decision": "allow", "token": Self.token,
        ])
        #expect(harness.backgroundTasks.begunNames.count == 1)
        #expect(harness.backgroundTasks.endedTokens.count == 1)
        #expect(harness.notices.posted.isEmpty)
    }

    @Test("Deny → decision 字段为 deny")
    func decisionDenyPostsDeny() async throws {
        let host = try Self.makeHost(base: Self.hostABase)
        let harness = Self.makeHarness(hosts: [host])
        await harness.http.queueSuccess(
            method: "POST", url: try Self.decisionURL(base: Self.hostABase), status: 204
        )
        let sessionId = try #require(UUID(uuidString: Self.sessionId))

        await harness.handler.handle(
            .decision(.deny, sessionId: sessionId, token: Self.token)
        )

        let request = try #require(await harness.http.recordedRequests.first)
        let body = try JSONDecoder().decode(
            [String: String].self, from: try #require(request.httpBody)
        )
        #expect(body["decision"] == "deny")
    }

    @Test("403（token 过期/已用）→ 本地通知兜底提示进 App；文案不含 token；背景任务成对")
    func decisionRejectedPostsExpiredNotice() async throws {
        let host = try Self.makeHost(base: Self.hostABase)
        let harness = Self.makeHarness(hosts: [host])
        await harness.http.queueSuccess(
            method: "POST", url: try Self.decisionURL(base: Self.hostABase), status: 403
        )
        let sessionId = try #require(UUID(uuidString: Self.sessionId))

        await harness.handler.handle(
            .decision(.allow, sessionId: sessionId, token: Self.token)
        )

        #expect(harness.notices.posted.count == 1)
        let notice = try #require(harness.notices.posted.first)
        #expect(notice.title == PushDecisionCopy.expiredTitle)
        #expect(notice.body == PushDecisionCopy.expiredBody)
        // capability token 用后即弃：绝不出现在任何兜底文案里。
        #expect(!notice.body.contains(Self.token) && !notice.title.contains(Self.token))
        #expect(harness.backgroundTasks.endedTokens.count == 1)
    }

    @Test("传输层失败 → 本地通知兜底（失败可见，绝不静默吞）；背景任务成对")
    func decisionTransportFailurePostsFailedNotice() async throws {
        let host = try Self.makeHost(base: Self.hostABase)
        let harness = Self.makeHarness(hosts: [host])
        await harness.http.queueFailure(
            method: "POST", url: try Self.decisionURL(base: Self.hostABase),
            error: URLError(.cannotConnectToHost)
        )
        let sessionId = try #require(UUID(uuidString: Self.sessionId))

        await harness.handler.handle(
            .decision(.allow, sessionId: sessionId, token: Self.token)
        )

        #expect(harness.notices.posted.count == 1)
        #expect(harness.notices.posted.first?.title == PushDecisionCopy.failedTitle)
        #expect(harness.backgroundTasks.begunNames.count == 1)
        #expect(harness.backgroundTasks.endedTokens.count == 1)
    }

    @Test("多主机 → 先 RO 查 /live-sessions（无 Origin）定位持有会话的主机，再对其 POST")
    func decisionMultiHostResolvesOwner() async throws {
        let hostA = try Self.makeHost(base: Self.hostABase)
        let hostB = try Self.makeHost(base: Self.hostBBase, name: "mini")
        let harness = Self.makeHarness(hosts: [hostA, hostB])
        try await Self.queueTwoHostProbe(harness)
        await harness.http.queueSuccess(
            method: "POST", url: try Self.decisionURL(base: Self.hostBBase), status: 204
        )
        let sessionId = try #require(UUID(uuidString: Self.sessionId))

        await harness.handler.handle(
            .decision(.allow, sessionId: sessionId, token: Self.token)
        )

        let requests = await harness.http.recordedRequests
        let gets = requests.filter { $0.httpMethod == "GET" }
        #expect(gets.count == 2)
        for get in gets { // RO 铁律：探查一律不带 Origin
            #expect(get.value(forHTTPHeaderField: "Origin") == nil)
        }
        let posts = requests.filter { $0.httpMethod == "POST" }
        #expect(posts.count == 1)
        #expect(posts.first?.url == (try Self.decisionURL(base: Self.hostBBase)))
        #expect(harness.notices.posted.isEmpty)
    }

    @Test("多主机但会话无处可寻 → 不 POST，本地通知兜底")
    func decisionUnresolvableHostFailsVisibly() async throws {
        let hostA = try Self.makeHost(base: Self.hostABase)
        let hostB = try Self.makeHost(base: Self.hostBBase, name: "mini")
        let harness = Self.makeHarness(hosts: [hostA, hostB])
        await harness.http.queueSuccess(
            method: "GET", url: try Self.liveSessionsURL(base: Self.hostABase),
            body: Self.liveSessionsBody(containing: nil)
        )
        await harness.http.queueSuccess(
            method: "GET", url: try Self.liveSessionsURL(base: Self.hostBBase),
            body: Self.liveSessionsBody(containing: nil)
        )
        let sessionId = try #require(UUID(uuidString: Self.sessionId))

        await harness.handler.handle(
            .decision(.allow, sessionId: sessionId, token: Self.token)
        )

        let posts = await harness.http.recordedRequests.filter { $0.httpMethod == "POST" }
        #expect(posts.isEmpty)
        #expect(harness.notices.posted.first?.title == PushDecisionCopy.failedTitle)
    }

    // MARK: - handle(.openSession)：默认点按路由

    @Test("单主机点按 → openSession(host, id)，不发决策 POST，也不开背景任务")
    func openSessionSingleHostRoutes() async throws {
        let host = try Self.makeHost(base: Self.hostABase)
        let harness = Self.makeHarness(hosts: [host])
        let sessionId = try #require(UUID(uuidString: Self.sessionId))

        await harness.handler.handle(.openSession(sessionId: sessionId))

        #expect(harness.recorder.opened.count == 1)
        #expect(harness.recorder.opened.first?.host == host)
        #expect(harness.recorder.opened.first?.sessionId == sessionId)
        #expect(await harness.http.recordedRequests.isEmpty)
        #expect(harness.backgroundTasks.begunNames.isEmpty)
    }

    @Test("多主机点按 → RO 定位后打开持有主机")
    func openSessionMultiHostResolves() async throws {
        let hostA = try Self.makeHost(base: Self.hostABase)
        let hostB = try Self.makeHost(base: Self.hostBBase, name: "mini")
        let harness = Self.makeHarness(hosts: [hostA, hostB])
        try await Self.queueTwoHostProbe(harness)
        let sessionId = try #require(UUID(uuidString: Self.sessionId))

        await harness.handler.handle(.openSession(sessionId: sessionId))

        #expect(harness.recorder.opened.first?.host == hostB)
    }

    @Test("无主机可配 → 不打开、不 crash（App 已被点按拉起，落在列表页即可）")
    func openSessionUnresolvedIsSafeNoOp() async throws {
        let harness = Self.makeHarness(hosts: [])
        let sessionId = try #require(UUID(uuidString: Self.sessionId))

        await harness.handler.handle(.openSession(sessionId: sessionId))

        #expect(harness.recorder.opened.isEmpty)
    }

    // MARK: - 其余分支

    @Test(".invalidPayload → 计数 +1，无网络、无通知、无路由")
    func invalidPayloadCountedOnly() async throws {
        let harness = Self.makeHarness(hosts: [])

        await harness.handler.handle(.invalidPayload)

        #expect(harness.handler.invalidPayloadCount == 1)
        #expect(await harness.http.recordedRequests.isEmpty)
        #expect(harness.notices.posted.isEmpty)
        #expect(harness.recorder.opened.isEmpty)
    }

    @Test(".dismissed → 完全 no-op")
    func dismissedIsNoOp() async throws {
        let harness = Self.makeHarness(hosts: [])

        await harness.handler.handle(.dismissed)

        #expect(harness.handler.invalidPayloadCount == 0)
        #expect(await harness.http.recordedRequests.isEmpty)
    }
}
