import Foundation
import Testing
@testable import CodexBarCore

/// Reader tests use fake executables only: no real
/// claude-swap install, no credentials, no Keychain access.
struct ClaudeSwapAccountReaderTests {
    private func makeFakeExecutable(_ script: String) throws -> String {
        let directory = FileManager.default.temporaryDirectory
            .appendingPathComponent("claude-swap-reader-tests-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
        let url = directory.appendingPathComponent("cswap")
        try "#!/bin/sh\n\(script)\n".write(to: url, atomically: true, encoding: .utf8)
        try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
        return url.path
    }

    @Test
    func `reads and parses a schema v1 list from the executable`() async throws {
        let path = try self.makeFakeExecutable("""
        [ "$1" = "--list" ] || exit 2
        [ "$2" = "--json" ] || exit 2
        cat <<'EOF'
        {"schemaVersion": 1, "activeAccountNumber": 1, "accounts": [
          {"number": 1, "email": "a@b.c", "active": true, "usageStatus": "ok",
           "usage": {
             "fiveHour": {"pct": 12.5},
             "scoped": [{"pct": 33.0, "name": "Fable", "resetsAt": "2026-07-21T08:00:00Z"}]
           }}
        ]}
        EOF
        """)

        let list = try await ClaudeSwapAccountReader.readAccountList(executablePath: path)
        #expect(list.activeAccountNumber == 1)
        #expect(list.accounts.first?.fiveHour?.usedPercent == 12.5)
        #expect(list.accounts.first?.scoped == [
            ClaudeSwapScopedUsageWindow(
                name: "Fable",
                usedPercent: 33,
                resetsAt: Date(timeIntervalSince1970: 1_784_620_800)),
        ])
    }

    @Test
    func `surfaces the error envelope from a non zero exit`() async throws {
        let path = try self.makeFakeExecutable("""
        echo '{"schemaVersion": 1, "error": {"type": "SwitchError", "message": "store locked"}}'
        exit 1
        """)

        await #expect(throws: ClaudeSwapListParserError.reportedError(type: "SwitchError", message: "store locked")) {
            try await ClaudeSwapAccountReader.readAccountList(executablePath: path)
        }
    }

    @Test
    func `terminates executables that exceed the timeout`() async throws {
        let path = try self.makeFakeExecutable("sleep 30")

        await #expect(throws: (any Error).self) {
            try await ClaudeSwapAccountReader.readAccountList(executablePath: path, timeout: 0.5)
        }
    }

    @Test
    func `rejects oversized output before parsing`() async throws {
        let path = try self.makeFakeExecutable("""
        i=0
        while [ $i -lt 5000 ]; do
          printf '%s' '{"filler": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}'
          i=$((i+1))
        done
        """)

        await #expect(throws: (any Error).self) {
            try await ClaudeSwapAccountReader.readAccountList(executablePath: path)
        }
    }

    @Test
    func `fails cleanly when the executable is missing`() async throws {
        await #expect(throws: (any Error).self) {
            try await ClaudeSwapAccountReader.readAccountList(
                executablePath: "/nonexistent/path/to/cswap")
        }
        await #expect(throws: ClaudeSwapAccountReaderError.self) {
            try await ClaudeSwapAccountReader.readAccountList(executablePath: "   ")
        }
    }

    @Test
    func `reads the executable version`() async throws {
        let path = try self.makeFakeExecutable("""
        [ "$1" = "--version" ] || exit 2
        echo 'cswap 0.16.0'
        """)

        let version = await ClaudeSwapAccountReader.readVersion(executablePath: path)
        #expect(version == "0.16.0")
    }

    @Test
    func `switches only by validated numeric slot with fixed arguments`() async throws {
        let path = try self.makeFakeExecutable("""
        [ "$1" = "--switch-to" ] || exit 2
        [ "$2" = "7" ] || exit 2
        [ "$3" = "--json" ] || exit 2
        [ -z "$4" ] || exit 2
        echo '{"schemaVersion":1,"switched":true,"from":{"number":1},"to":{"number":7},"reason":"switched"}'
        """)

        let result = try await ClaudeSwapAccountReader.switchAccount(
            executablePath: path,
            accountNumber: 7)

        #expect(result.switched)
        #expect(result.fromAccountNumber == 1)
        #expect(result.toAccountNumber == 7)
    }

    @Test
    func `rejects switch result for another slot`() async throws {
        let path = try self.makeFakeExecutable("""
        echo '{"schemaVersion":1,"switched":true,"from":{"number":1},"to":{"number":8},"reason":"switched"}'
        """)

        await #expect(throws: ClaudeSwapSwitchParserError.mismatchedTarget(expected: 7, actual: 8)) {
            try await ClaudeSwapAccountReader.switchAccount(executablePath: path, accountNumber: 7)
        }
    }

    @Test
    func `surfaces switch error envelope from non zero exit`() async throws {
        let path = try self.makeFakeExecutable("""
        echo '{"schemaVersion":1,"error":{"type":"SwitchError","message":"credentials missing"}}'
        exit 1
        """)

        await #expect(throws: ClaudeSwapSwitchParserError.reportedError(
            type: "SwitchError",
            message: "credentials missing"))
        {
            try await ClaudeSwapAccountReader.switchAccount(executablePath: path, accountNumber: 2)
        }
    }

    @Test
    func `started credential switch reaches natural exit after caller cancellation`() async throws {
        let marker = FileManager.default.temporaryDirectory
            .appendingPathComponent("claude-swap-switch-finished-\(UUID().uuidString)")
        let path = try self.makeFakeExecutable("""
        sleep 0.3
        touch '\(marker.path)'
        echo '{"schemaVersion":1,"switched":true,"from":{"number":1},"to":{"number":2},"reason":"switched"}'
        """)
        let task = Task {
            try await ClaudeSwapAccountReader.switchAccount(executablePath: path, accountNumber: 2)
        }

        try await Task.sleep(for: .milliseconds(100))
        task.cancel()
        let result = try await task.value

        #expect(result.switched)
        #expect(FileManager.default.fileExists(atPath: marker.path))
    }

    @Test
    func `version probe returns nil when the executable fails`() async throws {
        let path = try self.makeFakeExecutable("exit 3")

        let version = await ClaudeSwapAccountReader.readVersion(executablePath: path)
        #expect(version == nil)
    }

    @Test
    func `cancellation during version probe prevents account list launch`() async throws {
        let marker = FileManager.default.temporaryDirectory
            .appendingPathComponent("claude-swap-list-launched-\(UUID().uuidString)")
        let path = try self.makeFakeExecutable("""
        if [ "$1" = "--version" ]; then
          sleep 30
          exit 0
        fi
        touch '\(marker.path)'
        echo '{"schemaVersion":1,"activeAccountNumber":null,"accounts":[]}'
        """)
        let task = Task {
            _ = await ClaudeSwapAccountReader.readVersion(executablePath: path)
            return try await ClaudeSwapAccountReader.readAccountList(executablePath: path)
        }

        try await Task.sleep(for: .milliseconds(100))
        task.cancel()

        await #expect(throws: CancellationError.self) {
            try await task.value
        }
        #expect(!FileManager.default.fileExists(atPath: marker.path))
    }

    @Test
    func `expands tilde in configured paths`() throws {
        let resolved = try ClaudeSwapAccountReader.resolvedExecutablePath("~/bin/cswap")
        #expect(resolved.hasPrefix("/"))
        #expect(!resolved.contains("~"))
        #expect(resolved.hasSuffix("/bin/cswap"))
    }
}
