//
//  POISearchServiceTests.swift
//  SportsTimeTests
//
//  TDD specification tests for POISearchService types.
//

import Testing
import Foundation
import CoreLocation
@testable import SportsTime

// MARK: - POI Tests

@Suite("POI")
struct POITests {

    // MARK: - Test Data

    private func makePOI(distanceMeters: Double) -> POISearchService.POI {
        POISearchService.POI(
            id: UUID(),
            name: "Test POI",
            category: .restaurant,
            coordinate: CLLocationCoordinate2D(latitude: 40.0, longitude: -74.0),
            distanceMeters: distanceMeters,
            address: nil
        )
    }

    // MARK: - Specification Tests: formattedDistance

    /// - Expected Behavior: Distances < 0.1 miles format as feet
    @Test("formattedDistance: short distances show feet")
    func formattedDistance_feet() {
        // 100 meters = ~328 feet = ~0.062 miles (less than 0.1)
        let poi = makePOI(distanceMeters: 100)
        let formatted = poi.formattedDistance

        #expect(formatted.contains("ft"))
        #expect(!formatted.contains("mi"))
    }

    /// - Expected Behavior: Distances >= 0.1 miles format as miles
    @Test("formattedDistance: longer distances show miles")
    func formattedDistance_miles() {
        // 500 meters = ~0.31 miles (greater than 0.1)
        let poi = makePOI(distanceMeters: 500)
        let formatted = poi.formattedDistance

        #expect(formatted.contains("mi"))
        #expect(!formatted.contains("ft"))
    }

    /// - Expected Behavior: Boundary at 0.1 miles (~161 meters)
    @Test("formattedDistance: boundary at 0.1 miles")
    func formattedDistance_boundary() {
        // 0.1 miles = ~161 meters
        let justUnderPOI = makePOI(distanceMeters: 160) // Just under 0.1 miles
        let justOverPOI = makePOI(distanceMeters: 162)  // Just over 0.1 miles

        #expect(justUnderPOI.formattedDistance.contains("ft"))
        #expect(justOverPOI.formattedDistance.contains("mi"))
    }

    /// - Expected Behavior: Zero distance formats correctly
    @Test("formattedDistance: handles zero distance")
    func formattedDistance_zero() {
        let poi = makePOI(distanceMeters: 0)
        let formatted = poi.formattedDistance
        #expect(formatted.contains("0") || formatted.contains("ft"))
    }

    /// - Expected Behavior: Large distance formats correctly
    @Test("formattedDistance: handles large distance")
    func formattedDistance_large() {
        // 5000 meters = ~3.1 miles
        let poi = makePOI(distanceMeters: 5000)
        let formatted = poi.formattedDistance

        #expect(formatted.contains("mi"))
        #expect(formatted.contains("3.1") || formatted.contains("3.") || Double(formatted.replacingOccurrences(of: " mi", with: ""))! > 3.0)
    }

    // MARK: - Invariant Tests

    /// - Invariant: formattedDistance always contains a unit
    @Test("Invariant: formattedDistance always has unit")
    func invariant_formattedDistanceHasUnit() {
        let testDistances: [Double] = [0, 50, 100, 160, 162, 500, 1000, 5000]

        for distance in testDistances {
            let poi = makePOI(distanceMeters: distance)
            let formatted = poi.formattedDistance
            #expect(formatted.contains("ft") || formatted.contains("mi"))
        }
    }
}

// MARK: - POICategory Tests

@Suite("POICategory")
struct POICategoryTests {

    // MARK: - Specification Tests: displayName

    /// - Expected Behavior: Each category has a human-readable display name
    @Test("displayName: returns readable name")
    func displayName_readable() {
        #expect(POISearchService.POICategory.restaurant.displayName == "Restaurant")
        #expect(POISearchService.POICategory.attraction.displayName == "Attraction")
        #expect(POISearchService.POICategory.entertainment.displayName == "Entertainment")
        #expect(POISearchService.POICategory.nightlife.displayName == "Nightlife")
        #expect(POISearchService.POICategory.museum.displayName == "Museum")
    }

    // MARK: - Specification Tests: iconName

    /// - Expected Behavior: Each category has a valid SF Symbol name
    @Test("iconName: returns SF Symbol name")
    func iconName_sfSymbol() {
        #expect(POISearchService.POICategory.restaurant.iconName == "fork.knife")
        #expect(POISearchService.POICategory.attraction.iconName == "star.fill")
        #expect(POISearchService.POICategory.entertainment.iconName == "theatermasks.fill")
        #expect(POISearchService.POICategory.nightlife.iconName == "moon.stars.fill")
        #expect(POISearchService.POICategory.museum.iconName == "building.columns.fill")
    }

    // MARK: - Specification Tests: searchQuery

    /// - Expected Behavior: Each category has a search-friendly query string
    @Test("searchQuery: returns search string")
    func searchQuery_searchString() {
        #expect(POISearchService.POICategory.restaurant.searchQuery == "restaurants")
        #expect(POISearchService.POICategory.attraction.searchQuery == "tourist attractions")
        #expect(POISearchService.POICategory.entertainment.searchQuery == "entertainment")
        #expect(POISearchService.POICategory.nightlife.searchQuery == "bars nightlife")
        #expect(POISearchService.POICategory.museum.searchQuery == "museums")
    }

    // MARK: - Invariant Tests

    /// - Invariant: All categories have non-empty properties
    @Test("Invariant: all categories have non-empty properties")
    func invariant_nonEmptyProperties() {
        for category in POISearchService.POICategory.allCases {
            #expect(!category.displayName.isEmpty)
            #expect(!category.iconName.isEmpty)
            #expect(!category.searchQuery.isEmpty)
        }
    }

    /// - Invariant: CaseIterable includes all cases
    @Test("Invariant: CaseIterable includes all cases")
    func invariant_allCasesIncluded() {
        #expect(POISearchService.POICategory.allCases.count == 5)
        #expect(POISearchService.POICategory.allCases.contains(.restaurant))
        #expect(POISearchService.POICategory.allCases.contains(.attraction))
        #expect(POISearchService.POICategory.allCases.contains(.entertainment))
        #expect(POISearchService.POICategory.allCases.contains(.nightlife))
        #expect(POISearchService.POICategory.allCases.contains(.museum))
    }
}

// MARK: - POISearchError Tests

@Suite("POISearchError")
struct POISearchErrorTests {

    // MARK: - Specification Tests: errorDescription

    /// - Expected Behavior: searchFailed includes the reason
    @Test("errorDescription: searchFailed includes reason")
    func errorDescription_searchFailed() {
        let error = POISearchService.POISearchError.searchFailed("Network error")
        #expect(error.errorDescription != nil)
        #expect(error.errorDescription!.contains("Network error") || error.errorDescription!.lowercased().contains("search"))
    }

    /// - Expected Behavior: noResults explains no POIs found
    @Test("errorDescription: noResults mentions no results")
    func errorDescription_noResults() {
        let error = POISearchService.POISearchError.noResults
        #expect(error.errorDescription != nil)
        #expect(error.errorDescription!.lowercased().contains("no") || error.errorDescription!.lowercased().contains("found"))
    }

    // MARK: - Invariant Tests

    /// - Invariant: All errors have non-empty descriptions
    @Test("Invariant: all errors have descriptions")
    func invariant_allHaveDescriptions() {
        let errors: [POISearchService.POISearchError] = [
            .searchFailed("test"),
            .noResults
        ]

        for error in errors {
            #expect(error.errorDescription != nil)
            #expect(!error.errorDescription!.isEmpty)
        }
    }
}
