//
//  ChatViewModelPureLogicTests.swift
//  oAITests
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen

import Testing
@testable import oAI

@Suite("ChatViewModel pure static helpers")
struct ChatViewModelPureLogicTests {

    // MARK: - detectGoodbyePhrase

    @Test("Recognizes a clear farewell phrase")
    func detectsGoodbye() {
        #expect(ChatViewModel.detectGoodbyePhrase(in: "OK, bye!"))
        #expect(ChatViewModel.detectGoodbyePhrase(in: "That's all, thanks."))
        #expect(ChatViewModel.detectGoodbyePhrase(in: "Have a good day"))
    }

    @Test("Does not false-positive on a word that merely contains a phrase as a substring")
    func doesNotMatchSubstring() {
        // "bye" is a whole-word match target -- "goodbyeee" should not match "bye"
        // as a substring because of the \b word-boundary regex.
        #expect(!ChatViewModel.detectGoodbyePhrase(in: "goodbyeee is not a real word"))
    }

    @Test("Does not match ordinary polite phrases that aren't farewells")
    func doesNotMatchPoliteRequests() {
        #expect(!ChatViewModel.detectGoodbyePhrase(in: "Thanks, that's helpful!"))
        #expect(!ChatViewModel.detectGoodbyePhrase(in: "Done with step one, what's next?"))
    }

    @Test("Matching is case-insensitive")
    func matchIsCaseInsensitive() {
        #expect(ChatViewModel.detectGoodbyePhrase(in: "BYE BYE"))
    }

    // MARK: - inferProvider

    @Test("Model ID with a slash is inferred as OpenRouter")
    func infersOpenRouter() {
        #expect(ChatViewModel.inferProvider(from: "anthropic/claude-3-5-sonnet") == .openrouter)
    }

    @Test("claude- prefixed model is inferred as direct Anthropic")
    func infersAnthropic() {
        #expect(ChatViewModel.inferProvider(from: "claude-sonnet-4-5-20250929") == .anthropic)
    }

    @Test("gpt-/o1/o3/dall-e/chatgpt prefixed models are inferred as OpenAI")
    func infersOpenAI() {
        #expect(ChatViewModel.inferProvider(from: "gpt-4o") == .openai)
        #expect(ChatViewModel.inferProvider(from: "o1-preview") == .openai)
        #expect(ChatViewModel.inferProvider(from: "o3-mini") == .openai)
        #expect(ChatViewModel.inferProvider(from: "dall-e-3") == .openai)
        #expect(ChatViewModel.inferProvider(from: "chatgpt-4o-latest") == .openai)
    }

    @Test("A bare local model name with no recognized prefix falls back to Ollama")
    func fallsBackToOllama() {
        #expect(ChatViewModel.inferProvider(from: "llama3.2") == .ollama)
    }

    @Test("apple- prefixed model is inferred as Apple on-device")
    func infersAppleOnDevice() {
        #expect(ChatViewModel.inferProvider(from: "apple-on-device") == .appleOnDevice)
    }

    // MARK: - calculateCost

    @Test("Base prompt and completion cost with no cache usage")
    func calculatesBaseCost() {
        let usage = ChatResponse.Usage(promptTokens: 1_000_000, completionTokens: 1_000_000, totalTokens: 2_000_000)
        let pricing = ModelInfo.Pricing(prompt: 3.0, completion: 15.0)
        #expect(ChatViewModel.calculateCost(usage: usage, pricing: pricing) == 18.0)
    }

    @Test("Cache read tokens are charged at 0.1x the prompt rate")
    func calculatesCacheReadCost() {
        let usage = ChatResponse.Usage(
            promptTokens: 0,
            completionTokens: 0,
            totalTokens: 1_000_000,
            cacheReadInputTokens: 1_000_000
        )
        let pricing = ModelInfo.Pricing(prompt: 3.0, completion: 15.0)
        #expect(ChatViewModel.calculateCost(usage: usage, pricing: pricing) == 0.3)
    }

    @Test("Cache write tokens are charged at 1.25x the prompt rate")
    func calculatesCacheWriteCost() {
        let usage = ChatResponse.Usage(
            promptTokens: 0,
            completionTokens: 0,
            totalTokens: 1_000_000,
            cacheCreationInputTokens: 1_000_000
        )
        let pricing = ModelInfo.Pricing(prompt: 3.0, completion: 15.0)
        #expect(ChatViewModel.calculateCost(usage: usage, pricing: pricing) == 3.75)
    }

    @Test("Zero usage produces zero cost")
    func zeroUsageIsZeroCost() {
        let usage = ChatResponse.Usage(promptTokens: 0, completionTokens: 0, totalTokens: 0)
        let pricing = ModelInfo.Pricing(prompt: 3.0, completion: 15.0)
        #expect(ChatViewModel.calculateCost(usage: usage, pricing: pricing) == 0.0)
    }

    // MARK: - shouldAutoSave

    private func autoSaveEligible(
        syncEnabled: Bool = true,
        syncAutoSave: Bool = true,
        syncConfigured: Bool = true,
        isCloned: Bool = true,
        chatMessageCount: Int = 10,
        minMessages: Int = 4,
        lastSavedConversationId: String? = nil,
        currentConversationHash: String = "hash-a"
    ) -> Bool {
        ChatViewModel.shouldAutoSave(
            syncEnabled: syncEnabled,
            syncAutoSave: syncAutoSave,
            syncConfigured: syncConfigured,
            isCloned: isCloned,
            chatMessageCount: chatMessageCount,
            minMessages: minMessages,
            lastSavedConversationId: lastSavedConversationId,
            currentConversationHash: currentConversationHash
        )
    }

    @Test("Eligible when every criterion is satisfied")
    func autoSaveEligibleWhenAllCriteriaMet() {
        #expect(autoSaveEligible())
    }

    @Test("Not eligible when sync or auto-save is disabled")
    func autoSaveIneligibleWhenDisabled() {
        #expect(!autoSaveEligible(syncEnabled: false))
        #expect(!autoSaveEligible(syncAutoSave: false))
    }

    @Test("Not eligible when sync isn't configured or the repo isn't cloned")
    func autoSaveIneligibleWhenNotConfiguredOrCloned() {
        #expect(!autoSaveEligible(syncConfigured: false))
        #expect(!autoSaveEligible(isCloned: false))
    }

    @Test("Not eligible below the minimum message count")
    func autoSaveIneligibleBelowMinimumMessages() {
        #expect(!autoSaveEligible(chatMessageCount: 2, minMessages: 4))
    }

    @Test("Not eligible if this exact conversation was already auto-saved")
    func autoSaveIneligibleWhenAlreadySaved() {
        #expect(!autoSaveEligible(lastSavedConversationId: "hash-a", currentConversationHash: "hash-a"))
    }

    @Test("Eligible when the last saved hash differs from the current conversation")
    func autoSaveEligibleWhenConversationChanged() {
        #expect(autoSaveEligible(lastSavedConversationId: "hash-a", currentConversationHash: "hash-b"))
    }
}
