//
//  DataLayerTests.swift
//  honeyDueTests
//
//  Unit tests for the DATA layer domain (DATA-001 through DATA-007).
//  Exercises Kotlin DataManager directly from Swift without launching the app.
//
//  IMPORTANT: All suites that mutate DataManager (a shared singleton) are nested
//  inside a .serialized parent suite to prevent concurrent test interference.
//

import Testing
import Foundation
@testable import honeyDue
import ComposeApp

// MARK: - Serialized Parent Suite (prevents concurrent DataManager mutations)

@Suite(.serialized)
struct DataLayerTests {

    // MARK: - DATA-004: Cache Validation Tests

    @Suite struct CacheValidationTests {

        @Test func cacheTimeZeroIsInvalid() {
            #expect(DataManager.shared.isCacheValid(cacheTime: 0, ttlMs: DataManager.shared.CACHE_TIMEOUT_MS) == false)
        }

        @Test func recentCacheTimeIsValid() {
            // 5 minutes ago should be valid (well within the 1-hour timeout)
            let fiveMinutesAgo = Int64(Date().timeIntervalSince1970 * 1000) - (5 * 60 * 1000)
            #expect(DataManager.shared.isCacheValid(cacheTime: fiveMinutesAgo, ttlMs: DataManager.shared.CACHE_TIMEOUT_MS) == true)
        }

        @Test func expiredCacheTimeIsInvalid() {
            // 2 hours ago should be invalid (past the 1-hour timeout)
            let twoHoursAgo = Int64(Date().timeIntervalSince1970 * 1000) - (2 * 60 * 60 * 1000)
            #expect(DataManager.shared.isCacheValid(cacheTime: twoHoursAgo, ttlMs: DataManager.shared.CACHE_TIMEOUT_MS) == false)
        }

        @Test func cacheTimeoutConstantIsOneHour() {
            #expect(DataManager.shared.CACHE_TIMEOUT_MS == 3_600_000)
        }

        @Test func allCacheTimestampsStartAtZeroAfterClear() {
            DataManager.shared.clear()
            #expect(DataManager.shared.residencesCacheTime == 0)
            #expect(DataManager.shared.myResidencesCacheTime == 0)
            #expect(DataManager.shared.tasksCacheTime == 0)
            #expect(DataManager.shared.contractorsCacheTime == 0)
            #expect(DataManager.shared.documentsCacheTime == 0)
            #expect(DataManager.shared.summaryCacheTime == 0)
        }
    }

    // MARK: - DATA-005: Clear & ClearUserData Tests

    @Suite struct ClearTests {

        @Test func clearResetsAllCacheTimestamps() {
            let cat = TaskCategory(id: 1, name: "Test", description: "", icon: "", color: "", displayOrder: 0)
            DataManager.shared.setTaskCategories(categories: [cat])

            DataManager.shared.clear()

            #expect(DataManager.shared.residencesCacheTime == 0)
            #expect(DataManager.shared.myResidencesCacheTime == 0)
            #expect(DataManager.shared.tasksCacheTime == 0)
            #expect(DataManager.shared.contractorsCacheTime == 0)
            #expect(DataManager.shared.documentsCacheTime == 0)
            #expect(DataManager.shared.summaryCacheTime == 0)
        }

        @Test func clearEmptiesLookupLists() {
            // Populate all 5 lookup types
            DataManager.shared.setTaskCategories(categories: [
                TaskCategory(id: 1, name: "Cat", description: "", icon: "", color: "", displayOrder: 0)
            ])
            DataManager.shared.setTaskPriorities(priorities: [
                TaskPriority(id: 1, name: "High", level: 1, color: "red", displayOrder: 0)
            ])
            DataManager.shared.setTaskFrequencies(frequencies: [
                TaskFrequency(id: 1, name: "Weekly", days: nil, displayOrder: 0)
            ])
            DataManager.shared.setResidenceTypes(types: [
                ResidenceType(id: 1, name: "House")
            ])
            DataManager.shared.setContractorSpecialties(specialties: [
                ContractorSpecialty(id: 1, name: "Plumbing", description: nil, icon: nil, displayOrder: 0)
            ])

            DataManager.shared.clear()

            let categories = DataManager.shared.taskCategories.value as! [TaskCategory]
            let priorities = DataManager.shared.taskPriorities.value as! [TaskPriority]
            let frequencies = DataManager.shared.taskFrequencies.value as! [TaskFrequency]
            let residenceTypes = DataManager.shared.residenceTypes.value as! [ResidenceType]
            let specialties = DataManager.shared.contractorSpecialties.value as! [ContractorSpecialty]

            #expect(categories.isEmpty)
            #expect(priorities.isEmpty)
            #expect(frequencies.isEmpty)
            #expect(residenceTypes.isEmpty)
            #expect(specialties.isEmpty)
        }

        @Test func clearResetsLookupsInitializedFlag() {
            DataManager.shared.markLookupsInitialized()
            let before = DataManager.shared.lookupsInitialized.value as! Bool
            #expect(before == true)

            DataManager.shared.clear()

            let after = DataManager.shared.lookupsInitialized.value as! Bool
            #expect(after == false)
        }

        @Test func clearUserDataKeepsLookups() {
            DataManager.shared.clear()

            // Populate lookups
            DataManager.shared.setTaskCategories(categories: [
                TaskCategory(id: 1, name: "Plumbing", description: "", icon: "", color: "", displayOrder: 0)
            ])
            DataManager.shared.markLookupsInitialized()

            // Clear user data only
            DataManager.shared.clearUserData()

            // Lookups should remain
            let categories = DataManager.shared.taskCategories.value as! [TaskCategory]
            #expect(categories.count == 1)
            #expect(categories.first?.name == "Plumbing")

            let initialized = DataManager.shared.lookupsInitialized.value as! Bool
            #expect(initialized == true)

            // Clean up
            DataManager.shared.clear()
        }

        @Test func clearUserDataResetsCacheTimestamps() {
            DataManager.shared.clear()

            DataManager.shared.setTaskCategories(categories: [
                TaskCategory(id: 1, name: "Test", description: "", icon: "", color: "", displayOrder: 0)
            ])

            DataManager.shared.clearUserData()

            #expect(DataManager.shared.residencesCacheTime == 0)
            #expect(DataManager.shared.myResidencesCacheTime == 0)
            #expect(DataManager.shared.tasksCacheTime == 0)
            #expect(DataManager.shared.contractorsCacheTime == 0)
            #expect(DataManager.shared.documentsCacheTime == 0)
            #expect(DataManager.shared.summaryCacheTime == 0)

            DataManager.shared.clear()
        }
    }

    // MARK: - DATA-001, DATA-007: Lookup Setter & O(1) Getter Tests

    @Suite struct LookupSetterTests {

        @Test func setTaskCategoriesPopulatesList() {
            DataManager.shared.clear()

            let categories = [
                TaskCategory(id: 1, name: "Plumbing", description: "Water", icon: "wrench", color: "blue", displayOrder: 1),
                TaskCategory(id: 2, name: "HVAC", description: "Air", icon: "fan", color: "green", displayOrder: 2)
            ]
            DataManager.shared.setTaskCategories(categories: categories)

            let stored = DataManager.shared.taskCategories.value as! [TaskCategory]
            #expect(stored.count == 2)
            #expect(stored[0].name == "Plumbing")
            #expect(stored[1].name == "HVAC")

            DataManager.shared.clear()
        }

        @Test func setTaskCategoriesBuildsMappedLookup() {
            DataManager.shared.clear()

            let categories = [
                TaskCategory(id: 10, name: "Electrical", description: "", icon: "", color: "", displayOrder: 0),
                TaskCategory(id: 20, name: "Landscaping", description: "", icon: "", color: "", displayOrder: 1)
            ]
            DataManager.shared.setTaskCategories(categories: categories)

            // Verify the O(1) map-based getter works
            let result10 = DataManager.shared.getTaskCategory(id: 10)
            let result20 = DataManager.shared.getTaskCategory(id: 20)
            #expect(result10?.name == "Electrical")
            #expect(result20?.name == "Landscaping")

            DataManager.shared.clear()
        }

        @Test func setTaskPrioritiesPopulatesListAndMap() {
            DataManager.shared.clear()

            let priorities = [
                TaskPriority(id: 1, name: "High", level: 3, color: "red", displayOrder: 0),
                TaskPriority(id: 2, name: "Medium", level: 2, color: "yellow", displayOrder: 1),
                TaskPriority(id: 3, name: "Low", level: 1, color: "green", displayOrder: 2)
            ]
            DataManager.shared.setTaskPriorities(priorities: priorities)

            let stored = DataManager.shared.taskPriorities.value as! [TaskPriority]
            #expect(stored.count == 3)

            let high = DataManager.shared.getTaskPriority(id: 1)
            #expect(high?.name == "High")
            #expect(high?.level == 3)

            let low = DataManager.shared.getTaskPriority(id: 3)
            #expect(low?.name == "Low")

            DataManager.shared.clear()
        }

        @Test func setTaskFrequenciesPopulatesListAndMap() {
            DataManager.shared.clear()

            let frequencies = [
                TaskFrequency(id: 1, name: "Daily", days: 1, displayOrder: 0),
                TaskFrequency(id: 2, name: "Weekly", days: 7, displayOrder: 1)
            ]
            DataManager.shared.setTaskFrequencies(frequencies: frequencies)

            let stored = DataManager.shared.taskFrequencies.value as! [TaskFrequency]
            #expect(stored.count == 2)

            let daily = DataManager.shared.getTaskFrequency(id: 1)
            #expect(daily?.name == "Daily")

            let weekly = DataManager.shared.getTaskFrequency(id: 2)
            #expect(weekly?.name == "Weekly")

            DataManager.shared.clear()
        }

        @Test func setResidenceTypesPopulatesListAndMap() {
            DataManager.shared.clear()

            let types = [
                ResidenceType(id: 1, name: "Single Family"),
                ResidenceType(id: 2, name: "Condo"),
                ResidenceType(id: 3, name: "Townhouse")
            ]
            DataManager.shared.setResidenceTypes(types: types)

            let stored = DataManager.shared.residenceTypes.value as! [ResidenceType]
            #expect(stored.count == 3)

            let condo = DataManager.shared.getResidenceType(id: 2)
            #expect(condo?.name == "Condo")

            DataManager.shared.clear()
        }

        @Test func setContractorSpecialtiesPopulatesListAndMap() {
            DataManager.shared.clear()

            let specialties = [
                ContractorSpecialty(id: 1, name: "Plumbing", description: "Pipes", icon: "wrench", displayOrder: 0),
                ContractorSpecialty(id: 2, name: "Electrical", description: "Wiring", icon: "bolt", displayOrder: 1)
            ]
            DataManager.shared.setContractorSpecialties(specialties: specialties)

            let stored = DataManager.shared.contractorSpecialties.value as! [ContractorSpecialty]
            #expect(stored.count == 2)

            let plumbing = DataManager.shared.getContractorSpecialty(id: 1)
            #expect(plumbing?.name == "Plumbing")

            DataManager.shared.clear()
        }

        @Test func markLookupsInitializedSetsFlag() {
            DataManager.shared.clear()

            let before = DataManager.shared.lookupsInitialized.value as! Bool
            #expect(before == false)

            DataManager.shared.markLookupsInitialized()

            let after = DataManager.shared.lookupsInitialized.value as! Bool
            #expect(after == true)

            DataManager.shared.clear()
        }

        @Test func getterReturnsNilForMissingId() {
            DataManager.shared.clear()

            DataManager.shared.setTaskCategories(categories: [
                TaskCategory(id: 1, name: "Plumbing", description: "", icon: "", color: "", displayOrder: 0)
            ])

            let result = DataManager.shared.getTaskCategory(id: 9999)
            #expect(result == nil)

            DataManager.shared.clear()
        }

        @Test func getterReturnsNilForNilId() {
            DataManager.shared.clear()

            DataManager.shared.setTaskCategories(categories: [
                TaskCategory(id: 1, name: "Plumbing", description: "", icon: "", color: "", displayOrder: 0)
            ])

            let result = DataManager.shared.getTaskCategory(id: nil)
            #expect(result == nil)

            DataManager.shared.clear()
        }
    }
}

// MARK: - DATA-002: ETag Tests (requires local backend)

struct DataLayerETagTests {

    /// Base URL for direct HTTP calls to the static_data endpoint.
    /// Uses localhost for iOS simulator (127.0.0.1).
    private static let staticDataURL = "http://127.0.0.1:8000/api/static_data/"

    /// Synchronous HTTP GET with optional headers. Returns (statusCode, headers, data).
    private func syncRequest(
        url: String,
        headers: [String: String] = [:]
    ) -> (statusCode: Int, headers: [String: String], data: Data?) {
        let semaphore = DispatchSemaphore(value: 0)
        var statusCode = 0
        var responseHeaders: [String: String] = [:]
        var responseData: Data?

        var request = URLRequest(url: URL(string: url)!)
        request.timeoutInterval = 10
        for (key, value) in headers {
            request.setValue(value, forHTTPHeaderField: key)
        }

        let task = URLSession.shared.dataTask(with: request) { data, response, _ in
            if let httpResponse = response as? HTTPURLResponse {
                statusCode = httpResponse.statusCode
                for (key, value) in httpResponse.allHeaderFields {
                    responseHeaders["\(key)"] = "\(value)"
                }
            }
            responseData = data
            semaphore.signal()
        }
        task.resume()
        semaphore.wait()
        return (statusCode, responseHeaders, responseData)
    }

    /// Check if the local backend is reachable before running network tests.
    private func isBackendReachable() -> Bool {
        let semaphore = DispatchSemaphore(value: 0)
        var reachable = false

        var request = URLRequest(url: URL(string: Self.staticDataURL)!)
        request.timeoutInterval = 3
        request.httpMethod = "HEAD"

        let task = URLSession.shared.dataTask(with: request) { _, response, _ in
            if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode > 0 {
                reachable = true
            }
            semaphore.signal()
        }
        task.resume()
        semaphore.wait()
        return reachable
    }

    @Test func staticDataEndpointReturnsETag() throws {
        try #require(isBackendReachable(), "Local backend not reachable — skipping ETag test")

        let (statusCode, headers, _) = syncRequest(url: Self.staticDataURL)
        #expect(statusCode == 200)

        // ETag header should be present (case-insensitive check)
        let etag = headers["Etag"] ?? headers["ETag"] ?? headers["etag"]
        #expect(etag != nil, "Response should include an ETag header")
        #expect(etag?.isEmpty == false)
    }

    @Test func conditionalRequestReturns304WhenDataUnchanged() throws {
        try #require(isBackendReachable(), "Local backend not reachable — skipping ETag test")

        // First request: get the ETag
        let (statusCode1, headers1, _) = syncRequest(url: Self.staticDataURL)
        #expect(statusCode1 == 200)

        let etag = headers1["Etag"] ?? headers1["ETag"] ?? headers1["etag"]
        try #require(etag != nil, "First response must include ETag")

        // Second request: send If-None-Match with valid ETag
        let (statusCode2, headers2, _) = syncRequest(
            url: Self.staticDataURL,
            headers: ["If-None-Match": etag!]
        )

        // Server should return 304 (data unchanged) or 200 with a new ETag
        // (if data was modified between requests). Both are valid ETag behavior.
        let validResponses: Set<Int> = [200, 304]
        #expect(validResponses.contains(statusCode2),
                "Conditional request should return 304 (unchanged) or 200 (new data), got \(statusCode2)")

        if statusCode2 == 200 {
            // If server returned 200, it should include a new ETag
            let newEtag = headers2["Etag"] ?? headers2["ETag"] ?? headers2["etag"]
            #expect(newEtag != nil, "200 response should include a new ETag")
        }
    }

    @Test func staleETagReturns200() throws {
        try #require(isBackendReachable(), "Local backend not reachable — skipping ETag test")

        // Send a bogus ETag — server should return 200 with fresh data
        let (statusCode, _, data) = syncRequest(
            url: Self.staticDataURL,
            headers: ["If-None-Match": "\"bogus-etag-value\""]
        )
        #expect(statusCode == 200, "Stale/bogus ETag should result in 200 with fresh data")
        #expect(data != nil && (data?.count ?? 0) > 0)
    }
}

// MARK: - DATA-003, DATA-007: API Schema Validation Tests (requires local backend)

struct DataLayerAPISchemaTests {

    private static let staticDataURL = "http://127.0.0.1:8000/api/static_data/"

    /// Synchronous HTTP GET returning decoded JSON dictionary.
    private func fetchStaticData() -> [String: Any]? {
        let semaphore = DispatchSemaphore(value: 0)
        var result: [String: Any]?

        var request = URLRequest(url: URL(string: Self.staticDataURL)!)
        request.timeoutInterval = 10

        let task = URLSession.shared.dataTask(with: request) { data, response, _ in
            if let httpResponse = response as? HTTPURLResponse,
               httpResponse.statusCode == 200,
               let data = data {
                result = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
            }
            semaphore.signal()
        }
        task.resume()
        semaphore.wait()
        return result
    }

    private func isBackendReachable() -> Bool {
        let semaphore = DispatchSemaphore(value: 0)
        var reachable = false

        var request = URLRequest(url: URL(string: Self.staticDataURL)!)
        request.timeoutInterval = 3
        request.httpMethod = "HEAD"

        let task = URLSession.shared.dataTask(with: request) { _, response, _ in
            if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode > 0 {
                reachable = true
            }
            semaphore.signal()
        }
        task.resume()
        semaphore.wait()
        return reachable
    }

    @Test func staticDataContainsAllRequiredLookupTypes() throws {
        try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")

        let data = try #require(fetchStaticData(), "Failed to fetch static data")

        let requiredKeys = [
            "residence_types",
            "task_frequencies",
            "task_priorities",
            "task_categories",
            "contractor_specialties"
        ]

        for key in requiredKeys {
            #expect(data[key] != nil, "static_data response missing required key: \(key)")
            let array = data[key] as? [[String: Any]]
            #expect(array != nil, "\(key) should be an array of objects")
            #expect((array?.count ?? 0) > 0, "\(key) should not be empty")
        }
    }

    @Test func allLookupItemsHaveIdAndName() throws {
        try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")

        let data = try #require(fetchStaticData(), "Failed to fetch static data")

        let lookupKeys = [
            "residence_types",
            "task_frequencies",
            "task_priorities",
            "task_categories",
            "contractor_specialties"
        ]

        for key in lookupKeys {
            guard let items = data[key] as? [[String: Any]] else { continue }
            for (index, item) in items.enumerated() {
                #expect(item["id"] != nil, "\(key)[\(index)] missing 'id'")
                #expect(item["name"] != nil, "\(key)[\(index)] missing 'name'")
                #expect(item["name"] as? String != "", "\(key)[\(index)] has empty 'name'")
            }
        }
    }

    @Test func allIdsAreUniquePerLookupType() throws {
        try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")

        let data = try #require(fetchStaticData(), "Failed to fetch static data")

        let lookupKeys = [
            "residence_types",
            "task_frequencies",
            "task_priorities",
            "task_categories",
            "contractor_specialties"
        ]

        for key in lookupKeys {
            guard let items = data[key] as? [[String: Any]] else { continue }
            let ids = items.compactMap { $0["id"] as? Int }
            let uniqueIds = Set(ids)
            #expect(ids.count == uniqueIds.count, "\(key) has duplicate IDs — would break associateBy map")
        }
    }

    @Test func taskCategoriesHaveColorAndIcon() throws {
        try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")

        let data = try #require(fetchStaticData(), "Failed to fetch static data")
        let categories = try #require(data["task_categories"] as? [[String: Any]], "Missing task_categories")

        for (index, cat) in categories.enumerated() {
            #expect(cat["color"] != nil, "task_categories[\(index)] missing 'color'")
            #expect(cat["icon"] != nil, "task_categories[\(index)] missing 'icon'")
        }
    }

    @Test func taskPrioritiesHaveLevelAndColor() throws {
        try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")

        let data = try #require(fetchStaticData(), "Failed to fetch static data")
        let priorities = try #require(data["task_priorities"] as? [[String: Any]], "Missing task_priorities")

        for (index, pri) in priorities.enumerated() {
            #expect(pri["level"] != nil, "task_priorities[\(index)] missing 'level'")
            #expect(pri["color"] != nil, "task_priorities[\(index)] missing 'color'")
        }
    }
}
