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

import Testing
@testable import Confab

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

    // 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: - draftFingerprint

    @Test("Identical message content produces the same fingerprint")
    func draftFingerprintStableForSameContent() {
        let messages = [
            Message(role: .user, content: "Hello there"),
            Message(role: .assistant, content: "Hi! How can I help?")
        ]
        #expect(ChatViewModel.draftFingerprint(for: messages) == ChatViewModel.draftFingerprint(for: messages))
    }

    @Test("Changed message content produces a different fingerprint")
    func draftFingerprintChangesWithContent() {
        let before = [Message(role: .user, content: "Hello there")]
        let after = [Message(role: .user, content: "Hello there"), Message(role: .assistant, content: "Hi!")]
        #expect(ChatViewModel.draftFingerprint(for: before) != ChatViewModel.draftFingerprint(for: after))
    }

    @Test("Empty message list has a stable fingerprint")
    func draftFingerprintEmptyIsStable() {
        #expect(ChatViewModel.draftFingerprint(for: []) == ChatViewModel.draftFingerprint(for: []))
    }

    // MARK: - buildNotesSection

    @Test("Nil body produces no notes section at all")
    func buildNotesSectionNilBodyIsEmpty() {
        #expect(ChatViewModel.buildNotesSection(body: nil) == "")
    }

    @Test("Empty body still produces a section, with a placeholder for the notes content")
    func buildNotesSectionEmptyBodyShowsPlaceholder() {
        let section = ChatViewModel.buildNotesSection(body: "")
        #expect(section.contains("## Conversation Notes"))
        #expect(section.contains("nothing saved yet"))
    }

    @Test("Non-empty body is included verbatim")
    func buildNotesSectionIncludesBody() {
        let section = ChatViewModel.buildNotesSection(body: "User prefers dark roast coffee.")
        #expect(section.contains("User prefers dark roast coffee."))
    }

    // MARK: - extractNotesUpdate

    @Test("No fenced block leaves content untouched and returns no notes body")
    func extractNotesUpdateNoBlock() {
        let (display, body) = ChatViewModel.extractNotesUpdate(from: "Just a normal reply.")
        #expect(display == "Just a normal reply.")
        #expect(body == nil)
    }

    @Test("A fenced update-notes block is stripped from the display text and its body extracted")
    func extractNotesUpdateStripsBlock() {
        let content = "Sure, noted!\n\n```update-notes\nUser prefers dark roast coffee.\n```"
        let (display, body) = ChatViewModel.extractNotesUpdate(from: content)
        #expect(display == "Sure, noted!")
        #expect(body == "User prefers dark roast coffee.")
    }

    @Test("Text surrounding the fenced block on both sides is preserved")
    func extractNotesUpdatePreservesSurroundingText() {
        let content = "Before text.\n```update-notes\nRemember this.\n```\nAfter text."
        let (display, body) = ChatViewModel.extractNotesUpdate(from: content)
        #expect(display == "Before text.\n\nAfter text.")
        #expect(body == "Remember this.")
    }

    @Test("A block with the language tag but an empty body extracts an empty string, not nil")
    func extractNotesUpdateEmptyBody() {
        let content = "```update-notes\n```"
        let (_, body) = ChatViewModel.extractNotesUpdate(from: content)
        #expect(body == "")
    }
}
