import AppKit
import CodexBarCore
import Foundation
import Observation
import Testing
@testable import CodexBar

struct ProviderStorageFootprintTests {
    private final class ObservationFlag: @unchecked Sendable {
        private let lock = NSLock()
        private var value = false

        func set() {
            self.lock.lock()
            self.value = true
            self.lock.unlock()
        }

        func get() -> Bool {
            self.lock.lock()
            defer { self.lock.unlock() }
            return self.value
        }
    }

    @Test
    func `global low power mode clamps automatic storage scans to thirty minutes`() {
        #expect(UsageStore.automaticStorageRefreshInterval(lowPowerModeEnabled: false) == 5 * 60)
        #expect(UsageStore.automaticStorageRefreshInterval(lowPowerModeEnabled: true) == 30 * 60)
    }

    @Test
    func `scanner sums nested regular files and skips symlink targets`() throws {
        let root = try Self.makeTemporaryDirectory()
        defer { try? FileManager.default.removeItem(at: root) }

        let nested = root.appendingPathComponent("nested", isDirectory: true)
        try FileManager.default.createDirectory(at: nested, withIntermediateDirectories: true)
        try Data(repeating: 1, count: 5).write(to: root.appendingPathComponent("a.jsonl"))
        try Data(repeating: 2, count: 7).write(to: nested.appendingPathComponent("b.jsonl"))

        let external = try Self.makeTemporaryDirectory()
        defer { try? FileManager.default.removeItem(at: external) }
        let target = external.appendingPathComponent("outside.bin")
        try Data(repeating: 3, count: 100).write(to: target)
        let link = root.appendingPathComponent("linked.bin")
        try FileManager.default.createSymbolicLink(at: link, withDestinationURL: target)

        let footprint = ProviderStorageScanner().scan(provider: .codex, candidatePaths: [root.path])

        #expect(footprint.totalBytes == 12)
        #expect(footprint.paths == [root.path])
        #expect(footprint.missingPaths.isEmpty)
        #expect(footprint.components.map(\.name) == ["nested", "a.jsonl"])
        #expect(footprint.components.map(\.totalBytes) == [7, 5])
    }

    @Test
    func `scanner does not follow symlinked candidate roots`() throws {
        let root = try Self.makeTemporaryDirectory()
        defer { try? FileManager.default.removeItem(at: root) }

        let target = root.appendingPathComponent("target", isDirectory: true)
        try FileManager.default.createDirectory(at: target, withIntermediateDirectories: true)
        try Data(repeating: 1, count: 64).write(to: target.appendingPathComponent("session.jsonl"))
        let symlinkRoot = root.appendingPathComponent("codex-link", isDirectory: true)
        try FileManager.default.createSymbolicLink(at: symlinkRoot, withDestinationURL: target)

        let footprint = ProviderStorageScanner().scan(provider: .codex, candidatePaths: [symlinkRoot.path])

        #expect(footprint.paths == [symlinkRoot.path])
        #expect(footprint.totalBytes == 0)
        #expect(footprint.components.isEmpty)
    }

    @Test
    func `scanner records missing paths without failing`() throws {
        let root = try Self.makeTemporaryDirectory()
        let missing = root.appendingPathComponent("missing")
        defer { try? FileManager.default.removeItem(at: root) }

        let footprint = ProviderStorageScanner().scan(provider: .claude, candidatePaths: [missing.path])

        #expect(footprint.totalBytes == 0)
        #expect(footprint.paths.isEmpty)
        #expect(footprint.missingPaths == [missing.path])
    }

    @Test
    func `codex path catalog uses CODEX_HOME and managed homes`() {
        let managed = ManagedCodexAccount(
            id: UUID(),
            email: "user@example.com",
            managedHomePath: "/tmp/codex-managed-home",
            createdAt: 1,
            updatedAt: 1,
            lastAuthenticatedAt: nil)

        let paths = ProviderStoragePathCatalog.candidatePaths(
            for: .codex,
            environment: ["CODEX_HOME": "/tmp/codex-home"],
            managedCodexAccounts: [managed])

        #expect(paths == ["/tmp/codex-home", "/tmp/codex-managed-home"])
    }

    @Test
    func `codex path catalog falls back to default home`() {
        let paths = ProviderStoragePathCatalog.candidatePaths(for: .codex, environment: [:])
        let expected = FileManager.default.homeDirectoryForCurrentUser
            .appendingPathComponent(".codex", isDirectory: true)
            .path

        #expect(paths.first == expected)
    }

    @Test
    func `cursor path catalog includes application data and caches`() {
        let home = FileManager.default.homeDirectoryForCurrentUser
        let paths = ProviderStoragePathCatalog.candidatePaths(for: .cursor, environment: [:])

        #expect(paths == [
            home.appendingPathComponent("Library/Application Support/Cursor", isDirectory: true).path,
            home.appendingPathComponent(
                "Library/Application Support/Caches/cursor-updater",
                isDirectory: true).path,
            home.appendingPathComponent(".cursor", isDirectory: true).path,
            home.appendingPathComponent("Library/Caches/Cursor", isDirectory: true).path,
            home.appendingPathComponent("Library/Caches/com.todesktop.230313mzl4w4u92", isDirectory: true).path,
            home.appendingPathComponent("Library/Caches/com.todesktop.230313mzl4w4u92.ShipIt", isDirectory: true).path,
            home.appendingPathComponent("Library/Caches/cursor-compile-cache", isDirectory: true).path,
            home.appendingPathComponent(
                "Library/HTTPStorages/com.todesktop.230313mzl4w4u92",
                isDirectory: true).path,
        ])
    }

    @Test
    func `claude recommendations use documented cleanup categories`() {
        let root = "/Users/test/.claude"
        let footprint = ProviderStorageFootprint(
            provider: .claude,
            totalBytes: 28,
            paths: [root],
            missingPaths: [],
            unreadablePaths: [],
            components: [
                .init(path: "\(root)/projects", totalBytes: 10),
                .init(path: "\(root)/file-history", totalBytes: 8),
                .init(path: "\(root)/paste-cache", totalBytes: 6),
                .init(path: "\(root)/settings.json", totalBytes: 4),
            ],
            updatedAt: Date(timeIntervalSince1970: 0))

        let recommendations = footprint.cleanupRecommendations

        #expect(recommendations.map(\.path) == [
            "\(root)/projects",
            "\(root)/file-history",
            "\(root)/paste-cache",
        ])
        #expect(recommendations[0].consequence.contains("resume"))
        #expect(recommendations.allSatisfy { $0.riskLevel == .manualCleanup })
    }

    @Test
    func `codex recommendations stay under known homes and exclude auth and config`() {
        let root = "/Users/test/.codex"
        let footprint = ProviderStorageFootprint(
            provider: .codex,
            totalBytes: 51,
            paths: [root],
            missingPaths: [],
            unreadablePaths: [],
            components: [
                .init(path: "\(root)/sessions", totalBytes: 20),
                .init(path: "\(root)/archived_sessions", totalBytes: 15),
                .init(path: "\(root)/log", totalBytes: 12),
                .init(path: "\(root)/logs_2.sqlite", totalBytes: 11),
                .init(path: "\(root)/cache", totalBytes: 10),
                .init(path: "\(root)/shell_snapshots", totalBytes: 9),
                .init(path: "\(root)/auth.json", totalBytes: 4),
                .init(path: "\(root)/config.toml", totalBytes: 2),
                .init(path: "/tmp/outside/sessions", totalBytes: 99),
            ],
            updatedAt: Date(timeIntervalSince1970: 0))

        let recommendations = footprint.cleanupRecommendations

        #expect(recommendations.map(\.path) == [
            "\(root)/sessions",
            "\(root)/archived_sessions",
            "\(root)/cache",
            "\(root)/log",
            "\(root)/logs_2.sqlite",
            "\(root)/shell_snapshots",
        ])
        #expect(recommendations.map(\.bytes) == [20, 15, 10, 12, 11, 9])
    }

    @Test
    func `unknown provider storage returns no cleanup recommendations`() {
        let footprint = ProviderStorageFootprint(
            provider: .gemini,
            totalBytes: 10,
            paths: ["/Users/test/.gemini"],
            missingPaths: [],
            unreadablePaths: [],
            components: [
                .init(path: "/Users/test/.gemini/cache", totalBytes: 10),
            ],
            updatedAt: Date(timeIntervalSince1970: 0))

        #expect(footprint.cleanupRecommendations.isEmpty)
    }

    @Test
    @MainActor
    func `overview row carries storage text outside provider detail model`() throws {
        let now = Date()
        let metadata = try #require(ProviderDefaults.metadata[.claude])
        let snapshot = UsageSnapshot(
            primary: RateWindow(
                usedPercent: 25,
                windowMinutes: nil,
                resetsAt: now.addingTimeInterval(3600),
                resetDescription: nil),
            secondary: nil,
            updatedAt: now,
            identity: nil)
        let model = UsageMenuCardView.Model.make(.init(
            provider: .claude,
            metadata: metadata,
            snapshot: snapshot,
            credits: nil,
            creditsError: nil,
            dashboard: nil,
            dashboardError: nil,
            tokenSnapshot: nil,
            tokenError: nil,
            account: AccountInfo(email: nil, plan: nil),
            isRefreshing: false,
            lastError: nil,
            usageBarsShowUsed: false,
            resetTimeDisplayStyle: .countdown,
            tokenCostUsageEnabled: false,
            showOptionalCreditsAndExtraUsage: true,
            hidePersonalInfo: false,
            now: now))

        let overview = OverviewMenuCardRowView(model: model, storageText: "1.5 GB", width: 310)
        let detail = UsageMenuCardView(model: model, width: 310)

        #expect(overview.storageText == "1.5 GB")
        #expect(detail.model.provider == UsageProvider.claude)
    }

    @Test
    @MainActor
    func `storage detail view exposes cleanup recommendations while overview remains number only`() throws {
        let root = "/Users/test/.claude"
        let footprint = ProviderStorageFootprint(
            provider: .claude,
            totalBytes: 10,
            paths: [root],
            missingPaths: [],
            unreadablePaths: [],
            components: [
                .init(path: "\(root)/projects", totalBytes: 10),
            ],
            updatedAt: Date(timeIntervalSince1970: 0))
        let detailView = StorageBreakdownMenuView(footprint: footprint, width: 310)
        let metadata = try #require(ProviderDefaults.metadata[.claude])
        let model = UsageMenuCardView.Model.make(.init(
            provider: .claude,
            metadata: metadata,
            snapshot: nil,
            credits: nil,
            creditsError: nil,
            dashboard: nil,
            dashboardError: nil,
            tokenSnapshot: nil,
            tokenError: nil,
            account: AccountInfo(email: nil, plan: nil),
            isRefreshing: false,
            lastError: nil,
            usageBarsShowUsed: false,
            resetTimeDisplayStyle: .countdown,
            tokenCostUsageEnabled: false,
            showOptionalCreditsAndExtraUsage: true,
            hidePersonalInfo: false,
            now: Date(timeIntervalSince1970: 0)))
        let overview = OverviewMenuCardRowView(model: model, storageText: "10 B", width: 310)

        #expect(detailView.cleanupRecommendations.map(\.path) == ["\(root)/projects"])
        #expect(overview.storageText == "10 B")
    }

    @Test
    @MainActor
    func `storage detail view exposes copyable exact paths`() {
        let root = "/Users/test/.claude"
        let footprint = ProviderStorageFootprint(
            provider: .claude,
            totalBytes: 110,
            paths: [root],
            missingPaths: [],
            unreadablePaths: [],
            components: [
                .init(path: "\(root)/projects", totalBytes: 100),
                .init(path: "\(root)/file-history", totalBytes: 10),
            ],
            updatedAt: Date(timeIntervalSince1970: 0))
        let detailView = StorageBreakdownMenuView(footprint: footprint, width: 310)

        #expect(detailView.copyablePaths.contains("\(root)/projects"))
        #expect(detailView.copyablePaths.contains("\(root)/file-history"))
    }

    @Test
    @MainActor
    func `manual storage refresh updates deleted provider data`() async throws {
        let home = try Self.makeTemporaryDirectory()
        defer { try? FileManager.default.removeItem(at: home) }

        let codexHome = home.appendingPathComponent(".codex", isDirectory: true)
        let sessions = codexHome.appendingPathComponent("sessions", isDirectory: true)
        try FileManager.default.createDirectory(at: sessions, withIntermediateDirectories: true)
        try Data(repeating: 1, count: 32).write(to: sessions.appendingPathComponent("session.jsonl"))

        let suite = "ProviderStorageFootprintTests-storage-refresh-\(UUID().uuidString)"
        let defaults = try #require(UserDefaults(suiteName: suite))
        defaults.removePersistentDomain(forName: suite)
        let settings = SettingsStore(
            userDefaults: defaults,
            configStore: testConfigStore(suiteName: suite),
            zaiTokenStore: NoopZaiTokenStore(),
            syntheticTokenStore: NoopSyntheticTokenStore())
        if let codexMetadata = ProviderDefaults.metadata[.codex] {
            settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: true)
        }
        let store = UsageStore(
            fetcher: UsageFetcher(),
            browserDetection: BrowserDetection(cacheTTL: 0),
            settings: settings,
            environmentBase: ["CODEX_HOME": codexHome.path])
        settings.providerStorageFootprintsEnabled = true
        settings.backgroundWorkLowPowerModeEnabled = true
        store.managedCodexAccountsForStorageOverride = []

        await store.refreshStorageFootprintsForOverviewNow()
        #expect(store.storageFootprint(for: .codex)?.totalBytes == 32)

        try FileManager.default.removeItem(at: sessions)
        await store.refreshStorageFootprintsForOverviewNow()

        #expect(store.storageFootprint(for: .codex)?.totalBytes == 0)
        #expect(store.storageFootprintText(for: .codex) == "No local data found")
    }

    @Test
    @MainActor
    func `repeated identical storage refresh does not republish observable footprints`() async throws {
        let home = try Self.makeTemporaryDirectory()
        defer { try? FileManager.default.removeItem(at: home) }

        let codexHome = home.appendingPathComponent(".codex", isDirectory: true)
        let sessions = codexHome.appendingPathComponent("sessions", isDirectory: true)
        try FileManager.default.createDirectory(at: sessions, withIntermediateDirectories: true)
        try Data(repeating: 1, count: 32).write(to: sessions.appendingPathComponent("session.jsonl"))

        let suite = "ProviderStorageFootprintTests-identity-\(UUID().uuidString)"
        let defaults = try #require(UserDefaults(suiteName: suite))
        defaults.removePersistentDomain(forName: suite)
        let settings = SettingsStore(
            userDefaults: defaults,
            configStore: testConfigStore(suiteName: suite),
            zaiTokenStore: NoopZaiTokenStore(),
            syntheticTokenStore: NoopSyntheticTokenStore())
        if let codexMetadata = ProviderDefaults.metadata[.codex] {
            settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: true)
        }
        let store = UsageStore(
            fetcher: UsageFetcher(),
            browserDetection: BrowserDetection(cacheTTL: 0),
            settings: settings,
            environmentBase: ["CODEX_HOME": codexHome.path])
        settings.providerStorageFootprintsEnabled = true
        store.managedCodexAccountsForStorageOverride = []

        await store.refreshStorageFootprintsNow(for: [.codex])
        #expect(store.storageFootprint(for: .codex)?.totalBytes == 32)

        // A second scan over identical on-disk data must not re-assign the observable property.
        // Storage scans run on every menu open and every ~5 min; an unconditional re-publish wakes
        // the controller's `menuObservationToken` -> `invalidateMenus` path for no value change.
        let didRepublish = ObservationFlag()
        withObservationTracking {
            _ = store.providerStorageFootprints
        } onChange: {
            didRepublish.set()
        }
        await store.refreshStorageFootprintsNow(for: [.codex])
        try? await Task.sleep(for: .milliseconds(50))

        #expect(didRepublish.get() == false)
        #expect(store.storageFootprint(for: .codex)?.totalBytes == 32)
    }

    @Test
    @MainActor
    func `storage refresh is opt in and clears stale footprints when disabled`() async throws {
        let home = try Self.makeTemporaryDirectory()
        defer { try? FileManager.default.removeItem(at: home) }

        let codexHome = home.appendingPathComponent(".codex", isDirectory: true)
        try FileManager.default.createDirectory(at: codexHome, withIntermediateDirectories: true)
        try Data(repeating: 1, count: 16).write(to: codexHome.appendingPathComponent("session.jsonl"))

        let suite = "ProviderStorageFootprintTests-storage-opt-in-\(UUID().uuidString)"
        let defaults = try #require(UserDefaults(suiteName: suite))
        defaults.removePersistentDomain(forName: suite)
        let settings = SettingsStore(
            userDefaults: defaults,
            configStore: testConfigStore(suiteName: suite),
            zaiTokenStore: NoopZaiTokenStore(),
            syntheticTokenStore: NoopSyntheticTokenStore())
        if let codexMetadata = ProviderDefaults.metadata[.codex] {
            settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: true)
        }
        let store = UsageStore(
            fetcher: UsageFetcher(),
            browserDetection: BrowserDetection(cacheTTL: 0),
            settings: settings,
            environmentBase: ["CODEX_HOME": codexHome.path])
        store.managedCodexAccountsForStorageOverride = []

        await store.refreshStorageFootprintsForOverviewNow()
        #expect(store.storageFootprint(for: .codex) == nil)

        settings.providerStorageFootprintsEnabled = true
        await store.refreshStorageFootprintsForOverviewNow()
        #expect(store.storageFootprint(for: .codex)?.totalBytes == 16)

        settings.providerStorageFootprintsEnabled = false
        await store.refreshStorageFootprintsForOverviewNow()
        #expect(store.storageFootprint(for: .codex) == nil)
        #expect(store.providerStorageFootprints.isEmpty)
    }

    @Test
    @MainActor
    func `forced scheduled storage refresh does not restart identical in flight scan`() throws {
        let home = try Self.makeTemporaryDirectory()
        defer { try? FileManager.default.removeItem(at: home) }

        let codexHome = home.appendingPathComponent(".codex", isDirectory: true)
        try FileManager.default.createDirectory(at: codexHome, withIntermediateDirectories: true)

        let suite = "ProviderStorageFootprintTests-storage-in-flight-\(UUID().uuidString)"
        let defaults = try #require(UserDefaults(suiteName: suite))
        defaults.removePersistentDomain(forName: suite)
        let settings = SettingsStore(
            userDefaults: defaults,
            configStore: testConfigStore(suiteName: suite),
            zaiTokenStore: NoopZaiTokenStore(),
            syntheticTokenStore: NoopSyntheticTokenStore())
        let store = UsageStore(
            fetcher: UsageFetcher(),
            browserDetection: BrowserDetection(cacheTTL: 0),
            settings: settings,
            environmentBase: ["CODEX_HOME": codexHome.path])
        settings.providerStorageFootprintsEnabled = true
        store.storageRefreshGeneration = 41
        store.storageRefreshInFlightSignature = "codex=\(codexHome.path)"
        store.storageRefreshTask = Task.detached {
            try? await Task.sleep(for: .seconds(30))
        }
        defer {
            store.storageRefreshTask?.cancel()
            store.storageRefreshTask = nil
            store.storageRefreshInFlightSignature = nil
        }

        store.scheduleStorageFootprintRefresh(for: [.codex], force: true)

        #expect(store.storageRefreshGeneration == 41)
    }

    @Test
    @MainActor
    func `scheduled storage refresh notices managed Codex home changes`() async throws {
        let home = try Self.makeTemporaryDirectory()
        defer { try? FileManager.default.removeItem(at: home) }

        let ambientHome = home.appendingPathComponent("ambient", isDirectory: true)
        let firstManagedHome = home.appendingPathComponent("managed-a", isDirectory: true)
        let secondManagedHome = home.appendingPathComponent("managed-b", isDirectory: true)
        try FileManager.default.createDirectory(at: ambientHome, withIntermediateDirectories: true)
        try FileManager.default.createDirectory(at: firstManagedHome, withIntermediateDirectories: true)
        try FileManager.default.createDirectory(at: secondManagedHome, withIntermediateDirectories: true)
        try Data(repeating: 1, count: 16).write(to: firstManagedHome.appendingPathComponent("session.jsonl"))
        try Data(repeating: 2, count: 32).write(to: secondManagedHome.appendingPathComponent("session.jsonl"))

        let suite = "ProviderStorageFootprintTests-managed-refresh-\(UUID().uuidString)"
        let defaults = try #require(UserDefaults(suiteName: suite))
        defaults.removePersistentDomain(forName: suite)
        let settings = SettingsStore(
            userDefaults: defaults,
            configStore: testConfigStore(suiteName: suite),
            zaiTokenStore: NoopZaiTokenStore(),
            syntheticTokenStore: NoopSyntheticTokenStore())
        if let codexMetadata = ProviderDefaults.metadata[.codex] {
            settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: true)
        }
        let store = UsageStore(
            fetcher: UsageFetcher(),
            browserDetection: BrowserDetection(cacheTTL: 0),
            settings: settings,
            environmentBase: ["CODEX_HOME": ambientHome.path])
        settings.providerStorageFootprintsEnabled = true
        store.managedCodexAccountsForStorageOverride = [
            Self.managedCodexAccount(homePath: firstManagedHome.path),
        ]

        store.scheduleStorageFootprintRefresh(for: [.codex])
        for _ in 0..<100 where store.isStorageRefreshInFlight {
            try await Task.sleep(for: .milliseconds(10))
        }
        #expect(store.storageFootprint(for: .codex)?.totalBytes == 16)

        store.managedCodexAccountsForStorageOverride = [
            Self.managedCodexAccount(homePath: secondManagedHome.path),
        ]
        store.scheduleStorageFootprintRefresh(for: [.codex])
        for _ in 0..<100 where store.isStorageRefreshInFlight {
            try await Task.sleep(for: .milliseconds(10))
        }

        #expect(store.storageFootprint(for: .codex)?.totalBytes == 32)
    }

    private static func managedCodexAccount(homePath: String) -> ManagedCodexAccount {
        ManagedCodexAccount(
            id: UUID(),
            email: "storage@example.com",
            managedHomePath: homePath,
            createdAt: 1,
            updatedAt: 1,
            lastAuthenticatedAt: nil)
    }

    private static func makeTemporaryDirectory() throws -> URL {
        let url = FileManager.default.temporaryDirectory
            .appendingPathComponent("ProviderStorageFootprintTests-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
        return url
    }
}
