import AppKit
import CodexBarCore
import Testing
@testable import CodexBar

@MainActor
@Suite(.serialized)
struct StatusMenuHostedSubmenuRefreshTests {
    @Test
    func `claude swap completion changes open menu readiness`() {
        let settings = Self.makeSettings()
        settings.setProviderEnabled(
            provider: .claude,
            metadata: ProviderDescriptorRegistry.descriptor(for: .claude).metadata,
            enabled: true)
        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }

        let before = controller.menuAdjunctReadinessSignature()
        store.claudeSwapRevision &+= 1

        #expect(controller.menuAdjunctReadinessSignature() != before)
    }

    @Test
    func `status components change open menu readiness`() {
        let settings = Self.makeSettings()
        settings.statusChecksEnabled = true
        settings.setProviderEnabled(
            provider: .claude,
            metadata: ProviderDescriptorRegistry.descriptor(for: .claude).metadata,
            enabled: true)
        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }

        let before = controller.menuAdjunctReadinessSignature()
        store.statusComponents[.claude] = [
            ProviderStatusComponent(
                id: "api",
                name: "API",
                indicator: .none,
                status: "operational"),
        ]

        #expect(controller.menuAdjunctReadinessSignature() != before)
    }

    @Test
    func `project source changes open menu readiness`() {
        let settings = Self.makeSettings()
        settings.costUsageEnabled = true
        Self.enableOnly(settings, provider: .codex)
        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(projectSourcePath: "/tmp/main"), provider: .codex)
        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }

        let before = controller.menuAdjunctReadinessSignature()
        store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(projectSourcePath: "/tmp/worktree"), provider: .codex)

        #expect(controller.menuAdjunctReadinessSignature() != before)
    }

    @Test
    func `status submenu link stays scoped to its provider`() throws {
        let settings = Self.makeSettings()
        settings.statusChecksEnabled = true
        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }

        let submenu = NSMenu()
        #expect(controller.appendStatusComponentsItem(
            to: submenu,
            provider: .claude,
            width: StatusItemController.menuCardBaseWidth))
        #expect(controller.hydrateHostedSubviewMenuIfNeeded(submenu))

        let link = try #require(submenu.items.last)
        #expect(link.action == #selector(StatusItemController.openStatusPageFromMenuItem(_:)))
        #expect(link.identifier?.rawValue == UsageProvider.claude.rawValue)
        #expect(link.target === controller)
    }

    @Test
    func `storage native row preserves its plain menu title`() throws {
        let settings = Self.makeSettings()
        settings.providerStorageFootprintsEnabled = true
        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        Self.seedStorageFootprint(in: store)

        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }

        let menu = NSMenu()
        #expect(controller.addStorageMenuCardSection(
            to: menu,
            provider: .claude,
            width: StatusItemController.menuCardBaseWidth))
        let item = try #require(menu.items.first)
        #expect(item.title.hasPrefix(L("Storage")))
        #expect(item.title == item.attributedTitle?.string)
        #expect(item.view == nil)
        #expect(item.isEnabled)
        #expect(item.submenu != nil)
    }

    @Test
    func `open parent menu defers data rebuild until parent tracking ends`() async throws {
        let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
        StatusItemController.menuCardRenderingEnabled = true
        defer {
            StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering
        }

        let settings = Self.makeSettings()
        settings.statusChecksEnabled = false
        settings.refreshFrequency = .manual
        settings.mergeIcons = true
        settings.selectedMenuProvider = .claude
        settings.costUsageEnabled = true
        settings.costSummaryDisplayStyle = .both
        Self.enableOnlyClaude(settings)

        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        Self.seedClaudeSnapshots(in: store)

        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }
        controller.menuRefreshEnabledOverrideForTesting = false

        let menu = controller.makeMenu()
        controller.menuWillOpen(menu)
        let parentKey = ObjectIdentifier(menu)
        controller.openMenus[parentKey] = menu
        controller.menuVersions[parentKey] = controller.menuContentVersion

        let costItem = try #require(menu.items.first { ($0.representedObject as? String) == "menuCardCost" })
        #expect(costItem.view == nil)
        #expect(costItem.title == StatusItemController.costMenuTitleForProvider(.claude))
        #expect(costItem.isEnabled)
        let submenu = try #require(costItem.submenu)
        #expect(submenu.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
        #expect(submenu.minimumWidth >= StatusItemController.menuCardBaseWidth)
        #expect(submenu.items.first?.view == nil)

        controller.menuRefreshEnabledOverrideForTesting = true
        controller.menuWillOpen(submenu)
        let submenuKey = ObjectIdentifier(submenu)
        #expect(controller.openMenus[submenuKey] === submenu)
        #expect(submenu.items.first?.view != nil)

        let oldParentVersion = try #require(controller.menuVersions[parentKey])
        controller.invalidateMenus(
            refreshOpenMenus: true,
            deferOpenParentMenuRebuild: true)
        #expect(controller.menuVersions[parentKey] == oldParentVersion)
        controller.invalidateMenus(
            refreshOpenMenus: true,
            deferOpenParentMenuRebuild: true)
        #expect(controller.menuVersions[parentKey] == oldParentVersion)

        controller.menuDidClose(submenu)
        #expect(controller.openMenus[submenuKey] == nil)

        for _ in 0..<40 where controller.menuVersions[parentKey] != oldParentVersion {
            await Task.yield()
        }
        #expect(controller.menuVersions[parentKey] == oldParentVersion)

        controller.menuDidClose(menu)
        for _ in 0..<40 where controller.menuVersions[parentKey] != controller.menuContentVersion {
            await Task.yield()
        }
        if controller.menuVersions[parentKey] != controller.menuContentVersion {
            controller.menuWillOpen(menu)
        }
        for _ in 0..<40 where controller.menuVersions[parentKey] != controller.menuContentVersion {
            await Task.yield()
        }
        #expect(controller.menuVersions[parentKey] == controller.menuContentVersion)
    }

    @Test
    func `open hosted submenu rebuilds from unavailable placeholder when data arrives`() async {
        let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
        StatusItemController.menuCardRenderingEnabled = true
        defer {
            StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering
        }

        let settings = Self.makeSettings()
        settings.statusChecksEnabled = false
        settings.refreshFrequency = .manual
        settings.mergeIcons = true
        settings.selectedMenuProvider = .claude
        settings.costUsageEnabled = true
        Self.enableOnlyClaude(settings)

        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }
        controller.menuRefreshEnabledOverrideForTesting = true

        let submenu = controller.makeHostedSubviewPlaceholderMenu(
            chartID: StatusItemController.costHistoryChartID,
            provider: .claude,
            width: StatusItemController.menuCardBaseWidth)
        controller.menuWillOpen(submenu)
        let submenuKey = ObjectIdentifier(submenu)
        #expect(controller.openMenus[submenuKey] === submenu)
        #expect(submenu.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
        #expect(submenu.items.first?.view == nil)
        #expect(submenu.items.first?.title == "No data available")

        let openedVersion = controller.menuContentVersion
        store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(), provider: .claude)
        controller.invalidateMenus(refreshOpenMenus: true)

        for _ in 0..<40 {
            if controller.menuContentVersion != openedVersion,
               submenu.items.first?.view != nil
            {
                break
            }
            await Task.yield()
        }

        #expect(controller.menuContentVersion != openedVersion)
        #expect(submenu.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
        #expect(submenu.items.first?.view != nil)
        #expect(submenu.items.first?.title != "No data available")
    }

    @Test
    func `open hydrated provider submenu preserves identity across refresh`() throws {
        try self.assertHostedSubmenuPreservesIdentity(
            chartID: StatusItemController.costHistoryChartID,
            provider: .claude,
            seed: Self.seedClaudeSnapshots)
        try self.assertHostedSubmenuPreservesIdentity(
            chartID: StatusItemController.costHistoryChartID,
            provider: .openai,
            seed: Self.seedOpenAICostSnapshot)
        try self.assertHostedSubmenuPreservesIdentity(
            chartID: StatusItemController.usageHistoryChartID,
            provider: .claude,
            seed: Self.seedPlanUtilizationHistory)
        try self.assertHostedSubmenuPreservesIdentity(
            chartID: StatusItemController.storageBreakdownID,
            provider: .claude,
            seed: Self.seedStorageFootprint)
    }

    @Test
    func `hosted chart items size to the displayed view without a throwaway controller`() throws {
        try self.assertHostedChartItemHeightMatchesRefresh(
            chartID: StatusItemController.costHistoryChartID,
            provider: .claude,
            seed: Self.seedClaudeSnapshots)
        { controller, submenu, width in
            controller.appendCostHistoryChartItem(to: submenu, provider: .claude, width: width)
        }
        try self.assertHostedChartItemHeightMatchesRefresh(
            chartID: StatusItemController.usageHistoryChartID,
            provider: .claude,
            seed: Self.seedPlanUtilizationHistory)
        { controller, submenu, width in
            controller.appendUsageHistoryChartItem(to: submenu, provider: .claude, width: width)
        }
        try self.assertHostedChartItemHeightMatchesRefresh(
            chartID: StatusItemController.storageBreakdownID,
            provider: .claude,
            seed: Self.seedStorageFootprint)
        { controller, submenu, width in
            controller.appendStorageBreakdownItem(to: submenu, provider: .claude, width: width)
        }
    }

    @Test
    func `utilization chart invalidates when active account changes`() throws {
        let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
        StatusItemController.menuCardRenderingEnabled = true
        defer { StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering }

        let settings = Self.makeSettings()
        settings.statusChecksEnabled = false
        settings.refreshFrequency = .manual
        Self.enableOnlyClaude(settings)
        settings.addTokenAccount(provider: .claude, label: "Alice", token: "alice-token")
        settings.addTokenAccount(provider: .claude, label: "Bob", token: "bob-token")
        let accounts = settings.tokenAccounts(for: .claude)
        let alice = try #require(accounts.first)
        let bob = try #require(accounts.last)
        let aliceKey = try #require(
            UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: alice))
        let bobKey = try #require(
            UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: bob))

        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        Self.seedClaudeSnapshots(in: store)
        store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets(accounts: [
            aliceKey: [Self.makePlanHistory(usedPercent: 20)],
            bobKey: [Self.makePlanHistory(usedPercent: 50)],
        ])
        settings.setActiveTokenAccountIndex(0, for: .claude)

        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }

        let submenu = controller.makeHostedSubviewPlaceholderMenu(
            chartID: StatusItemController.usageHistoryChartID,
            provider: .claude,
            width: StatusItemController.menuCardBaseWidth)
        controller.menuWillOpen(submenu)
        let aliceView = try #require(submenu.items.first?.view)

        settings.setActiveTokenAccountIndex(1, for: .claude)
        controller.refreshHostedSubviewMenu(submenu)

        let bobView = try #require(submenu.items.first?.view)
        #expect(bobView !== aliceView)
    }

    private func assertHostedChartItemHeightMatchesRefresh(
        chartID: String,
        provider: UsageProvider,
        seed: (UsageStore) -> Void,
        append: (StatusItemController, NSMenu, CGFloat) -> Bool) throws
    {
        let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
        StatusItemController.menuCardRenderingEnabled = true
        defer { StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering }

        let settings = Self.makeSettings()
        settings.statusChecksEnabled = false
        settings.refreshFrequency = .manual
        settings.costUsageEnabled = true
        settings.providerStorageFootprintsEnabled = true
        Self.enableOnly(settings, provider: provider)

        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        seed(store)

        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }

        let width = StatusItemController.menuCardBaseWidth
        let submenu = NSMenu()
        submenu.minimumWidth = width
        #expect(append(controller, submenu, width))

        let item = try #require(submenu.items.first)
        let view = try #require(item.view)
        let heightFromAppend = view.frame.height
        // The height the append path assigns must match the authoritative re-measure pass; otherwise
        // dropping the throwaway NSHostingController would have changed sizing behavior.
        controller.refreshHostedSubviewHeights(in: submenu)
        #expect(view.frame.height == heightFromAppend)
        #expect(heightFromAppend > 1)
    }

    private func assertHostedSubmenuPreservesIdentity(
        chartID: String,
        provider: UsageProvider,
        seed: (UsageStore) -> Void) throws
    {
        let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
        StatusItemController.menuCardRenderingEnabled = true
        defer {
            StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering
        }

        let settings = Self.makeSettings()
        settings.statusChecksEnabled = false
        settings.refreshFrequency = .manual
        settings.mergeIcons = true
        settings.selectedMenuProvider = provider.instanceID
        settings.costUsageEnabled = true
        settings.providerStorageFootprintsEnabled = true
        Self.enableOnly(settings, provider: provider)

        let fetcher = UsageFetcher()
        let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
        seed(store)

        let controller = StatusItemController(
            store: store,
            settings: settings,
            account: fetcher.loadAccountInfo(),
            updater: DisabledUpdaterController(),
            preferencesSelection: PreferencesSelection(),
            statusBar: .system)
        defer { controller.releaseStatusItemsForTesting() }
        controller.menuRefreshEnabledOverrideForTesting = true

        let submenu = controller.makeHostedSubviewPlaceholderMenu(
            chartID: chartID,
            provider: provider,
            width: StatusItemController.menuCardBaseWidth)
        controller.menuWillOpen(submenu)

        let hydratedItem = try #require(submenu.items.first)
        #expect(hydratedItem.representedObject as? String == chartID)
        #expect(hydratedItem.toolTip == provider.rawValue)
        #expect(hydratedItem.view != nil)
        #expect(hydratedItem.title != "No data available")
        let hydratedView = hydratedItem.view

        controller.refreshHostedSubviewMenu(submenu)

        let refreshedItem = try #require(submenu.items.first)
        #expect(refreshedItem.representedObject as? String == chartID)
        #expect(refreshedItem.toolTip == provider.rawValue)
        #expect(refreshedItem.view != nil)
        #expect(refreshedItem.title != "No data available")
        #expect(refreshedItem.view === hydratedView)

        if chartID == StatusItemController.costHistoryChartID, provider == .claude {
            store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(dailyCost: 2.34), provider: .claude)
            controller.refreshHostedSubviewMenu(submenu)

            let changedItem = try #require(submenu.items.first)
            #expect(changedItem.view != nil)
            #expect(changedItem.view !== hydratedView)
        }
    }

    private static func makeSettings() -> SettingsStore {
        let suite = "StatusMenuHostedSubmenuRefreshTests-\(UUID().uuidString)"
        let defaults = UserDefaults(suiteName: suite)!
        defaults.removePersistentDomain(forName: suite)
        return SettingsStore(
            userDefaults: defaults,
            configStore: testConfigStore(suiteName: suite),
            zaiTokenStore: NoopZaiTokenStore(),
            syntheticTokenStore: NoopSyntheticTokenStore())
    }

    private static func enableOnlyClaude(_ settings: SettingsStore) {
        self.enableOnly(settings, provider: .claude)
    }

    private static func enableOnly(_ settings: SettingsStore, provider enabledProvider: UsageProvider) {
        let registry = ProviderRegistry.shared
        for provider in UsageProvider.allCases {
            guard let metadata = registry.metadata[provider] else { continue }
            settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == enabledProvider)
        }
    }

    private static func seedClaudeSnapshots(in store: UsageStore) {
        let snapshot = UsageSnapshot(
            primary: RateWindow(usedPercent: 10, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
            secondary: nil,
            tertiary: nil,
            updatedAt: Date(),
            identity: ProviderIdentitySnapshot(
                providerID: .claude,
                accountEmail: "user@example.com",
                accountOrganization: nil,
                loginMethod: "Team"))
        store._setSnapshotForTesting(snapshot, provider: .claude)
        store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(), provider: .claude)
    }

    private static func seedOpenAICostSnapshot(in store: UsageStore) {
        let day = Date(timeIntervalSince1970: 1_700_000_000)
        let apiUsage = OpenAIAPIUsageSnapshot(
            daily: [
                OpenAIAPIUsageSnapshot.DailyBucket(
                    day: "2025-12-23",
                    startTime: day,
                    endTime: day.addingTimeInterval(86400),
                    costUSD: 1.23,
                    requests: 12,
                    inputTokens: 100,
                    cachedInputTokens: 20,
                    outputTokens: 40,
                    totalTokens: 160,
                    lineItems: [],
                    models: []),
            ],
            updatedAt: Date(timeIntervalSince1970: 1_700_086_400))
        let snapshot = UsageSnapshot(
            primary: nil,
            secondary: nil,
            tertiary: nil,
            openAIAPIUsage: apiUsage,
            updatedAt: Date(timeIntervalSince1970: 1_700_086_400),
            identity: ProviderIdentitySnapshot(
                providerID: .openai,
                accountEmail: "openai@example.com",
                accountOrganization: nil,
                loginMethod: "API"))
        store._setSnapshotForTesting(snapshot, provider: .openai)
    }

    private static func seedPlanUtilizationHistory(in store: UsageStore) {
        self.seedClaudeSnapshots(in: store)
        store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets(
            unscoped: [
                self.makePlanHistory(usedPercent: 24),
            ])
    }

    private static func makePlanHistory(usedPercent: Double) -> PlanUtilizationSeriesHistory {
        PlanUtilizationSeriesHistory(
            name: .session,
            windowMinutes: 300,
            entries: [
                PlanUtilizationHistoryEntry(
                    capturedAt: Date(timeIntervalSince1970: 1_700_000_000),
                    usedPercent: usedPercent,
                    resetsAt: Date(timeIntervalSince1970: 1_700_018_000)),
            ])
    }

    private static func seedStorageFootprint(in store: UsageStore) {
        let root = "/Users/test/.claude"
        store.providerStorageFootprints[.claude] = ProviderStorageFootprint(
            provider: .claude,
            totalBytes: 1024,
            paths: [root],
            missingPaths: [],
            unreadablePaths: [],
            components: [.init(path: "\(root)/projects", totalBytes: 1024)],
            updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
    }

    private static func makeTokenSnapshot(
        dailyCost: Double = 1.23,
        projectSourcePath: String? = nil) -> CostUsageTokenSnapshot
    {
        let projects = projectSourcePath.map { sourcePath in
            [
                CostUsageProjectBreakdown(
                    name: "Project",
                    path: "/tmp/main",
                    totalTokens: 123,
                    totalCostUSD: dailyCost,
                    daily: [],
                    modelBreakdowns: nil,
                    sources: [
                        CostUsageProjectSourceBreakdown(
                            name: "Source",
                            path: sourcePath,
                            totalTokens: 123,
                            totalCostUSD: dailyCost,
                            daily: [],
                            modelBreakdowns: nil),
                    ]),
            ]
        } ?? []
        return CostUsageTokenSnapshot(
            sessionTokens: 123,
            sessionCostUSD: 0.12,
            last30DaysTokens: 123,
            last30DaysCostUSD: dailyCost,
            daily: [
                CostUsageDailyReport.Entry(
                    date: "2025-12-23",
                    inputTokens: nil,
                    outputTokens: nil,
                    totalTokens: 123,
                    costUSD: dailyCost,
                    modelsUsed: nil,
                    modelBreakdowns: nil),
            ],
            projects: projects,
            updatedAt: Date())
    }
}
