import Foundation
import Testing
@testable import OpenClaw

@MainActor
private final class ContinuationBox {
    var continuation: CheckedContinuation<Void, Never>?
}

@MainActor
private final class BrowserImportEligibilityGate {
    var isOnboarded = false
    var isLocalMode = false
}

@MainActor
private final class BrowserImportTransportStub {
    struct StubError: Error, LocalizedError {
        var errorDescription: String? {
            "boom"
        }
    }

    var requests: [BrowserProfileImportRequest] = []
    var failingPaths: Set<String> = []
    var beforeStatusResponse: (@MainActor () async -> Void)?
    var statusJSON = """
    {
      "enabled": true,
      "systemProfiles": [
        { "browser": "chrome", "id": "Default", "name": "Personal", "hasCookies": true }
      ],
      "state": null,
      "suggestedTarget": "imported"
    }
    """

    func makeModel(
        isOnboarded: @escaping @MainActor () -> Bool = { true },
        isLocalMode: @escaping @MainActor () -> Bool = { true }) -> BrowserProfileImportModel
    {
        BrowserProfileImportModel(
            transport: { [weak self] request in
                guard let self else { throw StubError() }
                self.requests.append(request)
                if request.path == "/system-profile-import/status", let hook = self.beforeStatusResponse {
                    self.beforeStatusResponse = nil
                    await hook()
                }
                if self.failingPaths.contains(request.path) {
                    throw StubError()
                }
                switch request.path {
                case "/system-profile-import/status":
                    return Data(self.statusJSON.utf8)
                case "/profiles/import":
                    return Data(#"{"into":"imported","cookies":{"total":412,"imported":409}}"#.utf8)
                default:
                    return Data(#"{"ok":true}"#.utf8)
                }
            },
            isOnboarded: isOnboarded,
            isLocalMode: isLocalMode)
    }

    func requests(for path: String) -> [BrowserProfileImportRequest] {
        self.requests.filter { $0.path == path }
    }
}

@MainActor
struct BrowserProfileImportModelTests {
    private static func status(
        profiles: [BrowserSystemProfile],
        enabled: Bool = true,
        state: BrowserProfileImportOutcome? = nil) -> BrowserProfileImportStatus
    {
        BrowserProfileImportStatus(
            enabled: enabled,
            systemProfiles: profiles,
            state: state,
            suggestedTarget: "imported")
    }

    private static let chromeProfile = BrowserSystemProfile(
        browser: "chrome",
        id: "Default",
        name: "Personal",
        hasCookies: true)

    @Test func `automatic offer requires importable cookies and no prior outcome`() {
        let fresh = Self.status(profiles: [Self.chromeProfile])
        #expect(BrowserProfileImportModel.shouldOffer(status: fresh, force: false))

        let dismissed = Self.status(
            profiles: [Self.chromeProfile],
            state: BrowserProfileImportOutcome(status: .dismissed))
        #expect(!BrowserProfileImportModel.shouldOffer(status: dismissed, force: false))
        #expect(BrowserProfileImportModel.shouldOffer(status: dismissed, force: true))
    }

    @Test func `profiles without cookies do not trigger import`() {
        let status = Self.status(profiles: [
            BrowserSystemProfile(browser: "brave", id: "Default", name: "Default", hasCookies: false),
        ])
        #expect(!BrowserProfileImportModel.shouldOffer(status: status, force: true))
    }

    @Test func `disabled import never offers`() {
        let status = Self.status(profiles: [Self.chromeProfile], enabled: false)
        #expect(!BrowserProfileImportModel.shouldOffer(status: status, force: true))
    }

    @Test func `refresh publishes the offer banner`() async {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel()
        let outcome = await model.refresh(force: false)
        #expect(outcome == .offering)
        guard case let .offering(status) = model.phase else {
            Issue.record("expected offering phase, got \(model.phase)")
            return
        }
        #expect(status.importableProfiles == [Self.chromeProfile])
    }

    @Test func `remote gateways hide the banner and report local-mode requirement`() async {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel(isLocalMode: { false })
        let outcome = await model.refresh(force: true)
        #expect(outcome == .unavailable(
            title: "Browser import requires Local mode",
            message: "Switch this Mac app to a local Gateway before importing browser cookies."))
        #expect(model.phase == .hidden)
        #expect(stub.requests.isEmpty)
    }

    @Test func `automatic offer request waits for onboarding and local mode`() async {
        let stub = BrowserImportTransportStub()
        let eligibility = BrowserImportEligibilityGate()
        let model = stub.makeModel(
            isOnboarded: { eligibility.isOnboarded },
            isLocalMode: { eligibility.isLocalMode })

        #expect(await !model.requestAutomaticOfferIfEligible())
        eligibility.isOnboarded = true
        #expect(await !model.requestAutomaticOfferIfEligible())
        eligibility.isLocalMode = true
        #expect(await model.requestAutomaticOfferIfEligible())
        #expect(stub.requests(for: "/system-profile-import/status").count == 1)
    }

    @Test func `failed automatic status request stays retryable`() async {
        let stub = BrowserImportTransportStub()
        stub.failingPaths = ["/system-profile-import/status"]
        let model = stub.makeModel()

        #expect(await !model.requestAutomaticOfferIfEligible())
        stub.failingPaths = []
        #expect(await model.requestAutomaticOfferIfEligible())
        #expect(stub.requests(for: "/system-profile-import/status").count == 2)
    }

    @Test func `automatic offer does not apply after its inline browser closes`() async {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel()
        let gate = ContinuationBox()
        var shouldApply = true
        stub.beforeStatusResponse = {
            await withCheckedContinuation { gate.continuation = $0 }
        }

        let request = Task {
            await model.requestAutomaticOfferIfEligible(while: { shouldApply })
        }
        while gate.continuation == nil {
            await Task.yield()
        }

        shouldApply = false
        gate.continuation?.resume()
        #expect(await !request.value)
        #expect(model.phase == .hidden)
    }

    @Test func `import success records counts and target`() async throws {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel()
        await model.refresh(force: false)
        await model.importProfile(Self.chromeProfile)

        let expected = BrowserProfileImportResult(
            into: "imported",
            cookies: BrowserProfileImportResult.Counts(total: 412, imported: 409))
        #expect(model.phase == .imported(expected))

        let importRequest = try #require(stub.requests(for: "/profiles/import").first)
        #expect(importRequest.method == "POST")
        #expect(importRequest.timeoutMs == 120_000)
        let body = try #require(importRequest.body)
        let object = try JSONSerialization.jsonObject(with: JSONEncoder().encode(body)) as? [String: Any]
        #expect(object?["browser"] as? String == "chrome")
        #expect(object?["systemProfile"] as? String == "Default")
        #expect(object?["into"] as? String == "imported")
        #expect(object?["makeDefault"] as? Bool == true)
    }

    @Test func `import failure keeps a retry path`() async {
        let stub = BrowserImportTransportStub()
        stub.failingPaths = ["/profiles/import"]
        let model = stub.makeModel()
        await model.refresh(force: false)
        await model.importProfile(Self.chromeProfile)

        guard case let .failed(message, retry) = model.phase else {
            Issue.record("expected failed phase, got \(model.phase)")
            return
        }
        #expect(message == "boom")
        #expect(retry.suggestedTarget == "imported")

        model.retry()
        #expect(model.phase == .offering(retry))
    }

    @Test func `dismissing an offer persists the dismissal`() async {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel()
        await model.refresh(force: false)
        model.dismiss()
        #expect(model.phase == .hidden)
        for _ in 0..<200 where stub.requests(for: "/system-profile-import/dismiss").isEmpty {
            await Task.yield()
        }
        let dismissals = stub.requests(for: "/system-profile-import/dismiss")
        #expect(dismissals.count == 1)
        #expect(dismissals.first?.method == "POST")
    }

    @Test func `dismissing a result banner does not overwrite the recorded outcome`() async {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel()
        await model.refresh(force: false)
        await model.importProfile(Self.chromeProfile)
        model.dismiss()
        #expect(model.phase == .hidden)
        for _ in 0..<50 {
            await Task.yield()
        }
        #expect(stub.requests(for: "/system-profile-import/dismiss").isEmpty)
    }

    @Test func `stale idle poll never clobbers a phase set while it was in flight`() async {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel()
        let gate = ContinuationBox()
        stub.beforeStatusResponse = {
            await withCheckedContinuation { gate.continuation = $0 }
        }

        let idle = Task { await model.refreshIfIdle() }
        while gate.continuation == nil {
            await Task.yield()
        }

        // A forced Settings offer lands while the idle poll is suspended.
        let forced = Self.status(
            profiles: [Self.chromeProfile],
            state: BrowserProfileImportOutcome(status: .dismissed))
        model._testSetPhase(.offering(forced))

        gate.continuation?.resume()
        #expect(await !idle.value)
        #expect(model.phase == .offering(forced))
    }

    @Test func `dismissal suppresses automatic re-offers for the session`() async {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel()
        await model.refresh(force: false)
        model.dismiss()
        #expect(model.phase == .hidden)

        // The persistence write may still be pending or lost; the server keeps
        // reporting an importable profile, but automatic polls must not re-offer.
        let statusPolls = stub.requests(for: "/system-profile-import/status").count
        let refreshed = await model.refreshIfIdle()
        #expect(!refreshed)
        #expect(stub.requests(for: "/system-profile-import/status").count == statusPolls)
        #expect(model.phase == .hidden)

        // Settings force-refresh still overrides the session suppression.
        let outcome = await model.refresh(force: true)
        #expect(outcome == .offering)
    }

    @Test func `stale idle poll does not resurrect a dismissed offer`() async {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel()
        let gate = ContinuationBox()
        stub.beforeStatusResponse = {
            await withCheckedContinuation { gate.continuation = $0 }
        }

        let idle = Task { await model.refreshIfIdle() }
        while gate.continuation == nil {
            await Task.yield()
        }

        // A faster poll offered the banner and the user dismissed it while the
        // slow poll was still waiting on its (pre-dismissal) status payload.
        model._testSetPhase(.offering(Self.status(profiles: [Self.chromeProfile])))
        model.dismiss()
        #expect(model.phase == .hidden)

        gate.continuation?.resume()
        _ = await idle.value
        #expect(model.phase == .hidden)
    }

    @Test func `idle refresh never clobbers a visible outcome`() async {
        let stub = BrowserImportTransportStub()
        let model = stub.makeModel()
        await model.refresh(force: false)
        await model.importProfile(Self.chromeProfile)
        let requestCount = stub.requests.count

        let refreshed = await model.refreshIfIdle()
        #expect(!refreshed)
        #expect(stub.requests.count == requestCount)
        guard case .imported = model.phase else {
            Issue.record("expected imported phase, got \(model.phase)")
            return
        }
    }
}

@MainActor
struct BrowserProfileImportBannerContentTests {
    @Test func `offering banner lists distinct browsers and offers all profiles`() throws {
        let profiles = [
            BrowserSystemProfile(browser: "chrome", id: "Default", name: "Personal", hasCookies: true),
            BrowserSystemProfile(browser: "chrome", id: "Profile 1", name: "Work", hasCookies: true),
            BrowserSystemProfile(browser: "brave", id: "Default", name: "Default", hasCookies: true),
        ]
        let status = BrowserProfileImportStatus(
            enabled: true,
            systemProfiles: profiles,
            state: nil,
            suggestedTarget: "imported")
        let content = try #require(BrowserProfileImportBannerContent.content(for: .offering(status)))
        #expect(content.title == "Use your browser logins")
        #expect(content.subtitle.contains("Chrome and Brave"))
        #expect(content.badge == .globe)
        #expect(content.action == .importProfiles(profiles))
    }

    @Test func `result banners carry counts and errors`() throws {
        let result = BrowserProfileImportResult(
            into: "imported",
            cookies: BrowserProfileImportResult.Counts(total: 10, imported: 9))
        let success = try #require(BrowserProfileImportBannerContent.content(for: .imported(result)))
        #expect(success.subtitle.contains("9 of 10 cookies"))
        #expect(success.badge == .success)
        #expect(success.action == .none)

        let status = BrowserProfileImportStatus(
            enabled: true,
            systemProfiles: [],
            state: nil,
            suggestedTarget: "imported")
        let failure = try #require(BrowserProfileImportBannerContent.content(
            for: .failed(message: "boom", retry: status)))
        #expect(failure.subtitle == "boom")
        #expect(failure.badge == .failure)
        #expect(failure.action == .retry)

        #expect(BrowserProfileImportBannerContent.content(for: .hidden) == nil)
    }
}
