import Foundation
import HostRegistry
import Testing
import WireProtocol
@testable import WebTerm

/// T-iOS-22 · Deep-link routing: `webterminal://open?host=<uuid>&join=<uuid>`
/// 解析（全字段白名单，任一非法 → .ignore，绝不部分应用）、WEBTERM_GATE push
/// payload 的同源校验入口，以及 DeepLinkHandler 的冷启动 stash / 未知 host
/// 落配对页 / 忽略计数。
@MainActor
@Suite("DeepLinkRouter")
struct DeepLinkRouterTests {
    // MARK: - Fixtures

    /// v4 UUID（version nibble=4、variant=8）——通过 Validation.isValidSessionId。
    private static let validHostId = "11111111-2222-4333-8444-555555555555"
    private static let validSessionId = "0f5a1f2e-3b4c-4d5e-8f6a-7b8c9d0e1f2a"
    /// version nibble=1：Foundation 的 UUID(uuidString:) 接受，但服务器的
    /// SESSION_ID_RE（v4 专属）拒绝——用它钉住"复用 Validation、不再造正则"。
    private static let v1StyleId = "11111111-2222-1333-8444-555555555555"
    /// variant nibble=c（合法 v4 要求 8/9/a/b）。
    private static let badVariantId = "11111111-2222-4333-c444-555555555555"

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

    private static func openURL(
        host: String = validHostId, join: String = validSessionId
    ) throws -> URL {
        try url("webterminal://open?host=\(host)&join=\(join)")
    }

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

    // MARK: - URL 解析：合法路径

    @Test("合法 open 链接 → .openSession(hostId, sessionId)")
    func validLinkRoutes() throws {
        let route = DeepLinkRouter.route(url: try Self.openURL())

        let hostId = try #require(UUID(uuidString: Self.validHostId))
        let sessionId = try #require(UUID(uuidString: Self.validSessionId))
        #expect(route == .openSession(hostId: hostId, sessionId: sessionId))
    }

    @Test("大写 UUID → 接受（Validation 大小写不敏感）")
    func uppercaseUUIDsAccepted() throws {
        let route = DeepLinkRouter.route(
            url: try Self.openURL(
                host: Self.validHostId.uppercased(),
                join: Self.validSessionId.uppercased()
            )
        )

        let hostId = try #require(UUID(uuidString: Self.validHostId))
        let sessionId = try #require(UUID(uuidString: Self.validSessionId))
        #expect(route == .openSession(hostId: hostId, sessionId: sessionId))
    }

    @Test("未知多余 query 键 → 忽略键本身，链接仍路由")
    func unknownExtraKeysIgnored() throws {
        let route = DeepLinkRouter.route(
            url: try Self.url(
                "webterminal://open?host=\(Self.validHostId)&join=\(Self.validSessionId)&utm=x&foo=bar"
            )
        )

        let hostId = try #require(UUID(uuidString: Self.validHostId))
        let sessionId = try #require(UUID(uuidString: Self.validSessionId))
        #expect(route == .openSession(hostId: hostId, sessionId: sessionId))
    }

    // MARK: - URL 解析：白名单拒绝路径（任一非法 → .ignore）

    @Test("scheme 非 webterminal → .ignore")
    func wrongSchemeIgnored() throws {
        let route = DeepLinkRouter.route(
            url: try Self.url("https://open?host=\(Self.validHostId)&join=\(Self.validSessionId)")
        )
        #expect(route == .ignore)
    }

    @Test("action 非 open → .ignore")
    func wrongActionIgnored() throws {
        let route = DeepLinkRouter.route(
            url: try Self.url("webterminal://kill?host=\(Self.validHostId)&join=\(Self.validSessionId)")
        )
        #expect(route == .ignore)
    }

    @Test("带非空 path → .ignore（白名单外的形状）")
    func extraPathIgnored() throws {
        let route = DeepLinkRouter.route(
            url: try Self.url(
                "webterminal://open/extra?host=\(Self.validHostId)&join=\(Self.validSessionId)"
            )
        )
        #expect(route == .ignore)
    }

    @Test("缺 host 键 → .ignore")
    func missingHostKeyIgnored() throws {
        let route = DeepLinkRouter.route(
            url: try Self.url("webterminal://open?join=\(Self.validSessionId)")
        )
        #expect(route == .ignore)
    }

    @Test("缺 join 键 → .ignore")
    func missingJoinKeyIgnored() throws {
        let route = DeepLinkRouter.route(
            url: try Self.url("webterminal://open?host=\(Self.validHostId)")
        )
        #expect(route == .ignore)
    }

    @Test("重复 host 键 → .ignore（歧义输入绝不部分应用）")
    func duplicateHostKeyIgnored() throws {
        let route = DeepLinkRouter.route(
            url: try Self.url(
                "webterminal://open?host=\(Self.validHostId)&host=\(Self.validHostId)&join=\(Self.validSessionId)"
            )
        )
        #expect(route == .ignore)
    }

    @Test("UUID 非 v4（version nibble=1）→ .ignore：钉住复用 Validation")
    func nonV4UUIDIgnored() throws {
        #expect(DeepLinkRouter.route(url: try Self.openURL(host: Self.v1StyleId)) == .ignore)
        #expect(DeepLinkRouter.route(url: try Self.openURL(join: Self.v1StyleId)) == .ignore)
    }

    @Test("variant nibble 非 8/9/a/b → .ignore")
    func badVariantUUIDIgnored() throws {
        #expect(DeepLinkRouter.route(url: try Self.openURL(join: Self.badVariantId)) == .ignore)
    }

    @Test("空值 / 垃圾值 / 空 query → .ignore，不 crash")
    func fuzzedValuesIgnored() throws {
        #expect(DeepLinkRouter.route(url: try Self.url("webterminal://open?host=&join=")) == .ignore)
        #expect(DeepLinkRouter.route(url: try Self.url("webterminal://open")) == .ignore)
        #expect(DeepLinkRouter.route(url: try Self.openURL(host: "not-a-uuid")) == .ignore)
        #expect(
            DeepLinkRouter.route(url: try Self.openURL(join: "'; DROP TABLE sessions;--")) == .ignore
        )
    }

    // MARK: - Push payload（WEBTERM_GATE，T-iOS-21 复用同一路由）

    @Test("payload 含合法 sessionId（多余键忽略）→ .gateSession")
    func gatePayloadRoutes() throws {
        let payload: [AnyHashable: Any] = [
            "sessionId": Self.validSessionId,
            "cls": "gate",
            "token": "opaque",
            "aps": ["category": "WEBTERM_GATE"],
        ]

        let sessionId = try #require(UUID(uuidString: Self.validSessionId))
        #expect(DeepLinkRouter.route(from: payload) == .gateSession(sessionId: sessionId))
    }

    @Test("payload 缺 sessionId / 非字符串 / 非 v4 → .ignore")
    func gatePayloadRejectsInvalid() {
        #expect(DeepLinkRouter.route(from: [:]) == .ignore)
        #expect(DeepLinkRouter.route(from: ["sessionId": 42]) == .ignore)
        #expect(DeepLinkRouter.route(from: ["sessionId": Self.v1StyleId]) == .ignore)
    }
}

/// DeepLinkHandler：热路径直达、冷启动 stash、未知 host 落配对、忽略计数。
@MainActor
@Suite("DeepLinkHandler")
struct DeepLinkHandlerTests {
    private enum StubError: Error { case storeFailure }

    /// 记录 handler 触发的动作（测试替身；生产侧由 AppCoordinator 提供闭包）。
    @MainActor
    private final class ActionRecorder {
        private(set) var opened: [(host: HostRegistry.Host, sessionId: UUID)] = []
        private(set) var pairingShownCount = 0

        var actions: DeepLinkHandler.Actions {
            DeepLinkHandler.Actions(
                openSession: { [weak self] host, sessionId in
                    self?.opened.append((host, sessionId))
                },
                showPairing: { [weak self] in
                    self?.pairingShownCount += 1
                }
            )
        }
    }

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

    private static func openURL(hostId: UUID, sessionId: UUID) throws -> URL {
        try #require(URL(
            string: "webterminal://open?host=\(hostId.uuidString)&join=\(sessionId.uuidString)"
        ))
    }

    /// UUID() 是 v4，但 uuidString 是大写——直接可作合法链接值。
    private static func sessionId() -> UUID { UUID() }

    // MARK: - 热路径（app 已就绪）

    @Test("热路径：已知 host → openSession(host, sessionId)，无 hint")
    func warmKnownHostOpens() async throws {
        let host = try Self.makeHost()
        let sessionId = Self.sessionId()
        let recorder = ActionRecorder()
        let handler = DeepLinkHandler(loadHosts: { [host] }, actions: recorder.actions)
        await handler.markReady()

        await handler.handle(url: try Self.openURL(hostId: host.id, sessionId: sessionId))

        #expect(recorder.opened.count == 1)
        #expect(recorder.opened.first?.host == host)
        #expect(recorder.opened.first?.sessionId == sessionId)
        #expect(recorder.pairingShownCount == 0)
        #expect(handler.hintMessage == nil)
    }

    @Test("未知 host id（UUID 合法但不在 store）→ showPairing + 提示文案")
    func unknownHostRoutesToPairing() async throws {
        let stored = try Self.makeHost()
        let recorder = ActionRecorder()
        let handler = DeepLinkHandler(loadHosts: { [stored] }, actions: recorder.actions)
        await handler.markReady()

        await handler.handle(
            url: try Self.openURL(hostId: UUID(), sessionId: Self.sessionId())
        )

        #expect(recorder.opened.isEmpty)
        #expect(recorder.pairingShownCount == 1)
        #expect(handler.hintMessage == DeepLinkCopy.unknownHostHint)
    }

    @Test("host store 读失败 → 显式错误文案，不 showPairing、不 crash")
    func storeFailureSurfacesExplicitCopy() async throws {
        let recorder = ActionRecorder()
        let handler = DeepLinkHandler(
            loadHosts: { throw StubError.storeFailure },
            actions: recorder.actions
        )
        await handler.markReady()

        await handler.handle(
            url: try Self.openURL(hostId: UUID(), sessionId: Self.sessionId())
        )

        #expect(recorder.opened.isEmpty)
        #expect(recorder.pairingShownCount == 0)
        #expect(handler.hintMessage == DeepLinkCopy.hostLoadFailed)
    }

    // MARK: - 非法链接：计数 + 不入 stash

    @Test("非法链接 → ignoredCount+1，无任何动作，也不入 stash")
    func invalidLinkCountedNeverStashed() async throws {
        let recorder = ActionRecorder()
        let handler = DeepLinkHandler(loadHosts: { [] }, actions: recorder.actions)

        await handler.handle(url: try #require(URL(string: "https://evil.example/?host=x")))
        #expect(handler.ignoredCount == 1)

        await handler.markReady() // ready 后 stash 若被污染会在此触发动作

        #expect(recorder.opened.isEmpty)
        #expect(recorder.pairingShownCount == 0)
        #expect(handler.hintMessage == nil)
    }

    // MARK: - 冷启动 stash

    @Test("冷启动：ready 前 handle → 挂起；markReady → 恰好应用一次")
    func coldLaunchStashAppliesOnReady() async throws {
        let host = try Self.makeHost()
        let sessionId = Self.sessionId()
        let recorder = ActionRecorder()
        let handler = DeepLinkHandler(loadHosts: { [host] }, actions: recorder.actions)

        await handler.handle(url: try Self.openURL(hostId: host.id, sessionId: sessionId))
        #expect(recorder.opened.isEmpty) // 未就绪：不得提前应用

        await handler.markReady()
        #expect(recorder.opened.count == 1)
        #expect(recorder.opened.first?.sessionId == sessionId)

        await handler.markReady() // 幂等：stash 已清空，不得重放
        #expect(recorder.opened.count == 1)
    }

    @Test("ready 前连续两条链接 → 只应用最新一条（单槽 stash）")
    func stashKeepsOnlyLatestLink() async throws {
        let host = try Self.makeHost()
        let first = Self.sessionId()
        let second = Self.sessionId()
        let recorder = ActionRecorder()
        let handler = DeepLinkHandler(loadHosts: { [host] }, actions: recorder.actions)

        await handler.handle(url: try Self.openURL(hostId: host.id, sessionId: first))
        await handler.handle(url: try Self.openURL(hostId: host.id, sessionId: second))
        await handler.markReady()

        #expect(recorder.opened.count == 1)
        #expect(recorder.opened.first?.sessionId == second)
    }

    // MARK: - hint 生命周期

    @Test("clearHint → hintMessage 归位 nil")
    func clearHintResets() async throws {
        let recorder = ActionRecorder()
        let handler = DeepLinkHandler(loadHosts: { [] }, actions: recorder.actions)
        await handler.markReady()
        await handler.handle(
            url: try Self.openURL(hostId: UUID(), sessionId: Self.sessionId())
        )
        #expect(handler.hintMessage != nil)

        handler.clearHint()

        #expect(handler.hintMessage == nil)
    }
}
