import Foundation
import Testing
import NucleicProtocol

@testable import NucleicCore

/// Mesh P5 — the source-side driver: a real `PeerClient` transfer channel driving a
/// `SessionTransferImporter` behind a real `SyncHost`, proving the production reply-routing
/// (SyncClient.Event → TransferReply through PeerClient) works end-to-end, not just the
/// in-memory loopback of `SessionTransferTests`.
@Suite struct PeerTransferChannelTests {
    let now = Date(timeIntervalSince1970: 1_700_000_000)

    /// A bridge that advertises the transfer capability and forwards transfer verbs to an importer.
    actor ImporterBridge: SyncHostBridge {
        let hostInfo = HostInfo(hostID: "host-B", hostName: "Dest Mac")
        let capabilities = WireCapabilities(
            canModifyToolInput: true, allowAlwaysScopes: [], canReceiveSessionTransfer: true)
        let modelCatalog = WireModelCatalog.empty
        let importer: SessionTransferImporter
        init(importer: SessionTransferImporter) { self.importer = importer }

        func sessionSummaries() async -> [WireSessionSummary] { [] }
        func snapshot(_ id: SessionID, sinceSeq: UInt64?) async -> SessionSnapshot? { nil }
        func respondToApproval(_ id: ApprovalID, _ decision: Decision, by responder: String) async throws {}
        func sendInput(_ id: SessionID, _ input: AgentInput) async throws {}
        func interrupt(_ id: SessionID) async throws {}
        func cancelQueuedMessage(_ id: SessionID, _ messageID: UUID) async throws {}
        func dashboardSnapshot() async -> DashboardSnapshot { .empty }
        func startChat(_ request: StartChatRequest) async throws {}
        func captureTodo(_ request: CaptureTodoRequest) async throws {}
        func dispatchTodo(_ id: TodoID, in projectID: ProjectID) async throws {}
        func remoteSetTodoStatus(_ id: TodoID, _ status: TodoStatus) async throws {}
        func remoteDeleteTodo(_ id: TodoID) async throws {}
        func renameSession(_ id: SessionID, to title: String) async throws {}
        func setFavorite(_ id: SessionID, _ favorite: Bool) async throws {}
        func setArchived(_ id: SessionID, _ archived: Bool) async throws {}
        func deleteSessionRemote(_ id: SessionID) async throws {}
        func integrate(_ id: SessionID, _ mode: IntegrationMode) async throws {}
        func discard(_ id: SessionID) async throws {}
        func setSessionModel(_ id: SessionID, _ model: String?) async throws {}
        func setSessionEffort(_ id: SessionID, _ effort: String?) async throws {}
        func setSessionAuto(_ id: SessionID, _ auto: Bool) async throws {}
        func setSessionAutoShip(_ id: SessionID, _ autoShip: Bool) async throws {}
        func setSessionShipBranch(_ id: SessionID, _ branch: String?) async throws {}
        func broadcasts() -> AsyncStream<HostBroadcast> { AsyncStream { _ in } }

        func receiveTransferOffer(_ offer: TransferOffer, from deviceID: String) async -> [HostMsg] {
            await importer.receiveOffer(offer, from: deviceID)
        }
        func receiveTransferChunk(_ chunk: TransferChunk) async -> [HostMsg] {
            await importer.receiveChunk(chunk)
        }
        func receiveTransferCommit(_ transferID: String) async -> [HostMsg] {
            await importer.receiveCommit(transferID)
        }
        func receiveTransferCancel(_ transferID: String) async { await importer.receiveCancel(transferID) }
    }

    @Test func endToEndTransferOverAPeerClientChannel() async throws {
        // Source repo + session (like SessionTransferTests.makeSource, condensed).
        let repo = try await GitTestRepo()
        defer { repo.cleanup() }
        let wm = GitWorktreeManager()
        let project = repo.project()
        let wt = try await wm.create(for: SessionID(rawValue: "s1"), in: project, base: "main", slug: "feature")
        try repo.write("code.txt", "work\n", in: wt.path)
        try await repo.run(["add", "-A"], in: wt.path)
        try await repo.run(["commit", "-m", "work"], in: wt.path)
        let tip = try await repo.revParse("HEAD", in: wt.path)

        let transcript = URL(fileURLWithPath: repo.container).appendingPathComponent("transcript.jsonl")
        let writer = try TranscriptWriter(url: transcript, header: SessionHeader(
            sessionID: SessionID(rawValue: "s1"), backend: .claudeCode, worktree: wt.path,
            createdAt: now))
        try await writer.append(AgentEvent(
            sessionID: SessionID(rawValue: "s1"), seq: 0, at: now, backend: .claudeCode,
            nativeType: nil, kind: .assistantText(TextChunk(messageID: "m", text: "hi", isPartial: false))))
        try await writer.close()

        let session = Session(
            id: SessionID(rawValue: "s1"), projectID: project.id, backend: .claudeCode,
            title: "feature", status: .awaitingInput, worktreePath: wt.path, branch: wt.branch,
            baseSHA: wt.baseSHA, lastSeq: 1, transcriptPath: transcript.path, createdAt: now, updatedAt: now)
        let srcStore = try GRDBMetadataStore(path: nil)
        try await srcStore.saveProject(project)
        try await srcStore.saveSession(session)

        // Destination: a clone + its own store + importer, behind a real SyncHost.
        let destContainer = (NSTemporaryDirectory() as NSString)
            .appendingPathComponent("nucleic-dest-\(UUID().uuidString)")
        let destRoot = (destContainer as NSString).appendingPathComponent("repo")
        try FileManager.default.createDirectory(atPath: destContainer, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(atPath: destContainer) }
        try await GitRunner().checked(["clone", repo.root, destRoot], in: destContainer)
        let destProject = Project(id: .generate(), name: "dest", rootPath: destRoot, defaultBranch: "main", createdAt: now)
        let destStore = try GRDBMetadataStore(path: nil)
        try await destStore.saveProject(destProject)
        let env = SessionTransferTests.FakeImportEnvironment(
            project: destProject,
            transcriptsDir: URL(fileURLWithPath: destContainer).appendingPathComponent("sessions"),
            nativeRoot: nil, onActivate: { _ in })
        let importer = SessionTransferImporter(
            store: destStore, worktrees: GitWorktreeManager(), environment: env,
            stagingRoot: URL(fileURLWithPath: destContainer).appendingPathComponent("xfer-dest"),
            now: { self.now })
        let bridge = ImporterBridge(importer: importer)

        let hostIdentity = DeviceIdentity()
        let host = SyncHost(identity: hostIdentity, bridge: bridge, store: InMemoryPairedDeviceStore())
        let listener = ManualListener()
        try await host.start(listener: listener)

        // Source PeerClient dials the host over the loopback dialer, pairs, and opens a channel.
        let dialer = LoopbackPeerDialer()
        dialer.route(host: "dest.local", to: listener)
        let srcIdentity = DeviceIdentity()
        let peerStore = InMemoryPairedDeviceStore()
        let peerClient = PeerClient(
            identity: srcIdentity, store: peerStore, dialer: dialer,
            config: PeerClient.Config(
                deviceID: srcIdentity.hostID, deviceLabel: "Source Mac",
                maxReconnectDelay: .milliseconds(50), idleRecheckDelay: .milliseconds(50)))
        _ = await peerClient.start()
        let payload = await host.beginPairing(lanHost: "dest.local", lanPort: 1)
        try await peerClient.pair(with: payload.qrString())

        // Wait for the managed connection to come up and advertise the transfer capability.
        let ready = await poll(timeout: .seconds(5)) {
            (await peerClient.presences()).filter { $0.connected && $0.canReceiveSessionTransfer }
        }
        let destDeviceID = try #require(ready.first?.deviceID)
        #expect(await peerClient.canTransfer(to: destDeviceID))

        // Drive the transfer through the real PeerClient channel.
        let channel = try #require(await peerClient.openTransferChannel(to: destDeviceID))
        let package = try SessionCarbonCopy.prepare(
            transferID: "xfer-1", session: session, project: project,
            projectDescriptor: ProjectDescriptor(
                projectID: project.id, name: project.name, rootCommitSHA: nil,
                normalizedRemote: nil, defaultBranch: "main"),
            worktree: wt, transcriptURL: transcript, nativeTranscriptURL: nil,
            branchTipSHA: tip, transcriptHeaderVersion: SessionHeader.currentVersion,
            stagingRoot: URL(fileURLWithPath: repo.container).appendingPathComponent("xfer-src"),
            worktrees: wm)
        let coordinator = SessionTransferCoordinator(
            session: session, project: project, worktree: wt, destinationDeviceID: destDeviceID,
            package: package, channel: channel, store: srcStore, worktrees: wm)
        try await coordinator.run(now: now)
        await peerClient.closeTransferChannel(to: destDeviceID)

        // Source tombstoned; destination has the live session.
        let sourceSession = try await srcStore.loadSession(id: session.id)
        #expect(sourceSession?.archived == true)
        #expect(sourceSession?.movedToDeviceID == destDeviceID)
        let destSession = try await destStore.loadSession(id: session.id)
        #expect(destSession != nil)
        #expect(destSession?.branch == "nucleic/feature")

        await peerClient.stop()
        await host.stop()
    }
}
