import Foundation
import XCTest

// MARK: - API Result Type

/// Result of an API call with status code access for error assertions.
struct APIResult<T> {
    let data: T?
    let statusCode: Int
    let errorBody: String?

    var succeeded: Bool { (200...299).contains(statusCode) }

    /// Unwrap data or fail the test.
    func unwrap(file: StaticString = #filePath, line: UInt = #line) -> T {
        guard let data = data else {
            XCTFail("Expected data but got status \(statusCode): \(errorBody ?? "nil")", file: file, line: line)
            preconditionFailure("unwrap failed")
        }
        return data
    }
}

// MARK: - Auth Response Types

struct TestUser: Decodable {
    let id: Int
    let username: String
    let email: String
    let firstName: String?
    let lastName: String?
    let isActive: Bool?
    let verified: Bool?

    enum CodingKeys: String, CodingKey {
        case id, username, email
        case firstName = "first_name"
        case lastName = "last_name"
        case isActive = "is_active"
        case verified
    }
}

struct TestAuthResponse: Decodable {
    let token: String
    let user: TestUser
    let message: String?
}

struct TestVerifyEmailResponse: Decodable {
    let message: String
    let verified: Bool
}

struct TestVerifyResetCodeResponse: Decodable {
    let message: String
    let resetToken: String

    enum CodingKeys: String, CodingKey {
        case message
        case resetToken = "reset_token"
    }
}

struct TestMessageResponse: Decodable {
    let message: String
}

struct TestSession {
    let token: String
    let user: TestUser
    let username: String
    let password: String
}

// MARK: - CRUD Response Types

/// Wrapper for create/update/get responses that include a summary.
struct TestWrappedResponse<T: Decodable>: Decodable {
    let data: T
}

struct TestResidence: Decodable {
    let id: Int
    let name: String
    let ownerId: Int?
    let streetAddress: String?
    let city: String?
    let stateProvince: String?
    let postalCode: String?
    let isPrimary: Bool?
    let isActive: Bool?

    enum CodingKeys: String, CodingKey {
        case id, name
        case ownerId = "owner_id"
        case streetAddress = "street_address"
        case city
        case stateProvince = "state_province"
        case postalCode = "postal_code"
        case isPrimary = "is_primary"
        case isActive = "is_active"
    }
}

struct TestTask: Decodable {
    let id: Int
    let residenceId: Int
    let title: String
    let description: String?
    let inProgress: Bool?
    let isCancelled: Bool?
    let isArchived: Bool?
    let kanbanColumn: String?

    enum CodingKeys: String, CodingKey {
        case id, title, description
        case residenceId = "residence_id"
        case inProgress = "in_progress"
        case isCancelled = "is_cancelled"
        case isArchived = "is_archived"
        case kanbanColumn = "kanban_column"
    }
}

struct TestContractor: Decodable {
    let id: Int
    let name: String
    let company: String?
    let phone: String?
    let email: String?
    let isFavorite: Bool?
    let isActive: Bool?

    enum CodingKeys: String, CodingKey {
        case id, name, company, phone, email
        case isFavorite = "is_favorite"
        case isActive = "is_active"
    }
}

struct TestDocument: Decodable {
    let id: Int
    let residenceId: Int
    let title: String
    let documentType: String?
    let isActive: Bool?

    enum CodingKeys: String, CodingKey {
        case id, title
        case residenceId = "residence_id"
        case documentType = "document_type"
        case isActive = "is_active"
    }
}

struct TestShareCode: Decodable {
    let id: Int
    let code: String
    let residenceId: Int
    let isActive: Bool

    enum CodingKeys: String, CodingKey {
        case id, code
        case residenceId = "residence_id"
        case isActive = "is_active"
    }
}

struct TestGenerateShareCodeResponse: Decodable {
    let message: String
    let shareCode: TestShareCode

    enum CodingKeys: String, CodingKey {
        case message
        case shareCode = "share_code"
    }
}

struct TestGetShareCodeResponse: Decodable {
    let shareCode: TestShareCode

    enum CodingKeys: String, CodingKey {
        case shareCode = "share_code"
    }
}

struct TestJoinResidenceResponse: Decodable {
    let message: String
    let residence: TestResidence
}

struct TestResidenceUser: Decodable {
    let id: Int
    let username: String
    let email: String

    enum CodingKeys: String, CodingKey {
        case id, username, email
    }
}

// MARK: - API Client

enum TestAccountAPIClient {
    static let baseURL = "http://127.0.0.1:8000/api"
    static let debugVerificationCode = "123456"

    // MARK: - Auth Methods

    static func register(username: String, email: String, password: String) -> TestAuthResponse? {
        let body: [String: Any] = [
            "username": username,
            "email": email,
            "password": password
        ]
        return performRequest(method: "POST", path: "/auth/register/", body: body, responseType: TestAuthResponse.self)
    }

    static func login(username: String, password: String) -> TestAuthResponse? {
        let body: [String: Any] = ["username": username, "password": password]
        return performRequest(method: "POST", path: "/auth/login/", body: body, responseType: TestAuthResponse.self)
    }

    static func verifyEmail(token: String) -> TestVerifyEmailResponse? {
        let body: [String: Any] = ["code": debugVerificationCode]
        return performRequest(method: "POST", path: "/auth/verify-email/", body: body, token: token, responseType: TestVerifyEmailResponse.self)
    }

    static func getCurrentUser(token: String) -> TestUser? {
        return performRequest(method: "GET", path: "/auth/me/", token: token, responseType: TestUser.self)
    }

    static func forgotPassword(email: String) -> TestMessageResponse? {
        let body: [String: Any] = ["email": email]
        return performRequest(method: "POST", path: "/auth/forgot-password/", body: body, responseType: TestMessageResponse.self)
    }

    static func verifyResetCode(email: String) -> TestVerifyResetCodeResponse? {
        let body: [String: Any] = ["email": email, "code": debugVerificationCode]
        return performRequest(method: "POST", path: "/auth/verify-reset-code/", body: body, responseType: TestVerifyResetCodeResponse.self)
    }

    static func resetPassword(resetToken: String, newPassword: String) -> TestMessageResponse? {
        let body: [String: Any] = ["reset_token": resetToken, "new_password": newPassword]
        return performRequest(method: "POST", path: "/auth/reset-password/", body: body, responseType: TestMessageResponse.self)
    }

    static func logout(token: String) -> TestMessageResponse? {
        return performRequest(method: "POST", path: "/auth/logout/", token: token, responseType: TestMessageResponse.self)
    }

    /// Convenience: register + verify + re-login, returns ready session.
    static func createVerifiedAccount(username: String, email: String, password: String) -> TestSession? {
        guard let registerResponse = register(username: username, email: email, password: password) else { return nil }
        guard verifyEmail(token: registerResponse.token) != nil else { return nil }
        guard let loginResponse = login(username: username, password: password) else { return nil }
        return TestSession(token: loginResponse.token, user: loginResponse.user, username: username, password: password)
    }

    // MARK: - Auth with Status Code

    /// Login returning full APIResult so callers can assert on 401, 400, etc.
    static func loginWithResult(username: String, password: String) -> APIResult<TestAuthResponse> {
        let body: [String: Any] = ["username": username, "password": password]
        return performRequestWithResult(method: "POST", path: "/auth/login/", body: body, responseType: TestAuthResponse.self)
    }

    /// Hit a protected endpoint without a token to get the 401.
    static func getCurrentUserWithResult(token: String?) -> APIResult<TestUser> {
        return performRequestWithResult(method: "GET", path: "/auth/me/", token: token, responseType: TestUser.self)
    }

    // MARK: - Residence CRUD

    static func createResidence(token: String, name: String, fields: [String: Any] = [:]) -> TestResidence? {
        var body: [String: Any] = ["name": name]
        for (k, v) in fields { body[k] = v }
        let wrapped: TestWrappedResponse<TestResidence>? = performRequest(
            method: "POST", path: "/residences/", body: body, token: token,
            responseType: TestWrappedResponse<TestResidence>.self
        )
        return wrapped?.data
    }

    static func listResidences(token: String) -> [TestResidence]? {
        return performRequest(method: "GET", path: "/residences/", token: token, responseType: [TestResidence].self)
    }

    static func updateResidence(token: String, id: Int, fields: [String: Any]) -> TestResidence? {
        let wrapped: TestWrappedResponse<TestResidence>? = performRequest(
            method: "PUT", path: "/residences/\(id)/", body: fields, token: token,
            responseType: TestWrappedResponse<TestResidence>.self
        )
        return wrapped?.data
    }

    static func deleteResidence(token: String, id: Int) -> Bool {
        let result: APIResult<TestWrappedResponse<String>> = performRequestWithResult(
            method: "DELETE", path: "/residences/\(id)/", token: token,
            responseType: TestWrappedResponse<String>.self
        )
        return result.succeeded
    }

    // MARK: - Task CRUD

    static func createTask(token: String, residenceId: Int, title: String, fields: [String: Any] = [:]) -> TestTask? {
        var body: [String: Any] = ["residence_id": residenceId, "title": title]
        for (k, v) in fields { body[k] = v }
        let wrapped: TestWrappedResponse<TestTask>? = performRequest(
            method: "POST", path: "/tasks/", body: body, token: token,
            responseType: TestWrappedResponse<TestTask>.self
        )
        return wrapped?.data
    }

    static func listTasks(token: String) -> [TestTask]? {
        return performRequest(method: "GET", path: "/tasks/", token: token, responseType: [TestTask].self)
    }

    static func listTasksByResidence(token: String, residenceId: Int) -> [TestTask]? {
        return performRequest(
            method: "GET", path: "/tasks/by-residence/\(residenceId)/", token: token,
            responseType: [TestTask].self
        )
    }

    static func updateTask(token: String, id: Int, fields: [String: Any]) -> TestTask? {
        let wrapped: TestWrappedResponse<TestTask>? = performRequest(
            method: "PUT", path: "/tasks/\(id)/", body: fields, token: token,
            responseType: TestWrappedResponse<TestTask>.self
        )
        return wrapped?.data
    }

    static func deleteTask(token: String, id: Int) -> Bool {
        let result: APIResult<TestWrappedResponse<String>> = performRequestWithResult(
            method: "DELETE", path: "/tasks/\(id)/", token: token,
            responseType: TestWrappedResponse<String>.self
        )
        return result.succeeded
    }

    static func markTaskInProgress(token: String, id: Int) -> TestTask? {
        let wrapped: TestWrappedResponse<TestTask>? = performRequest(
            method: "POST", path: "/tasks/\(id)/mark-in-progress/", token: token,
            responseType: TestWrappedResponse<TestTask>.self
        )
        return wrapped?.data
    }

    static func cancelTask(token: String, id: Int) -> TestTask? {
        let wrapped: TestWrappedResponse<TestTask>? = performRequest(
            method: "POST", path: "/tasks/\(id)/cancel/", token: token,
            responseType: TestWrappedResponse<TestTask>.self
        )
        return wrapped?.data
    }

    static func uncancelTask(token: String, id: Int) -> TestTask? {
        let wrapped: TestWrappedResponse<TestTask>? = performRequest(
            method: "POST", path: "/tasks/\(id)/uncancel/", token: token,
            responseType: TestWrappedResponse<TestTask>.self
        )
        return wrapped?.data
    }

    // MARK: - Contractor CRUD

    static func createContractor(token: String, name: String, fields: [String: Any] = [:]) -> TestContractor? {
        var body: [String: Any] = ["name": name]
        for (k, v) in fields { body[k] = v }
        return performRequest(method: "POST", path: "/contractors/", body: body, token: token, responseType: TestContractor.self)
    }

    static func listContractors(token: String) -> [TestContractor]? {
        return performRequest(method: "GET", path: "/contractors/", token: token, responseType: [TestContractor].self)
    }

    static func updateContractor(token: String, id: Int, fields: [String: Any]) -> TestContractor? {
        return performRequest(method: "PUT", path: "/contractors/\(id)/", body: fields, token: token, responseType: TestContractor.self)
    }

    static func deleteContractor(token: String, id: Int) -> Bool {
        let result: APIResult<TestMessageResponse> = performRequestWithResult(
            method: "DELETE", path: "/contractors/\(id)/", token: token,
            responseType: TestMessageResponse.self
        )
        return result.succeeded
    }

    static func toggleContractorFavorite(token: String, id: Int) -> TestContractor? {
        return performRequest(method: "POST", path: "/contractors/\(id)/toggle-favorite/", token: token, responseType: TestContractor.self)
    }

    // MARK: - Document CRUD

    static func createDocument(token: String, residenceId: Int, title: String, documentType: String = "general", fields: [String: Any] = [:]) -> TestDocument? {
        var body: [String: Any] = ["residence_id": residenceId, "title": title, "document_type": documentType]
        for (k, v) in fields { body[k] = v }
        return performRequest(method: "POST", path: "/documents/", body: body, token: token, responseType: TestDocument.self)
    }

    static func listDocuments(token: String) -> [TestDocument]? {
        return performRequest(method: "GET", path: "/documents/", token: token, responseType: [TestDocument].self)
    }

    static func updateDocument(token: String, id: Int, fields: [String: Any]) -> TestDocument? {
        return performRequest(method: "PUT", path: "/documents/\(id)/", body: fields, token: token, responseType: TestDocument.self)
    }

    static func deleteDocument(token: String, id: Int) -> Bool {
        let result: APIResult<TestMessageResponse> = performRequestWithResult(
            method: "DELETE", path: "/documents/\(id)/", token: token,
            responseType: TestMessageResponse.self
        )
        return result.succeeded
    }

    // MARK: - Residence Sharing

    static func generateShareCode(token: String, residenceId: Int) -> TestShareCode? {
        let wrapped: TestGenerateShareCodeResponse? = performRequest(
            method: "POST", path: "/residences/\(residenceId)/generate-share-code/",
            body: [:], token: token,
            responseType: TestGenerateShareCodeResponse.self
        )
        return wrapped?.shareCode
    }

    static func getShareCode(token: String, residenceId: Int) -> TestShareCode? {
        let wrapped: TestGetShareCodeResponse? = performRequest(
            method: "GET", path: "/residences/\(residenceId)/share-code/",
            token: token, responseType: TestGetShareCodeResponse.self
        )
        return wrapped?.shareCode
    }

    static func joinWithCode(token: String, code: String) -> TestJoinResidenceResponse? {
        let body: [String: Any] = ["code": code]
        return performRequest(
            method: "POST", path: "/residences/join-with-code/",
            body: body, token: token,
            responseType: TestJoinResidenceResponse.self
        )
    }

    static func removeUser(token: String, residenceId: Int, userId: Int) -> Bool {
        let result: APIResult<TestMessageResponse> = performRequestWithResult(
            method: "DELETE", path: "/residences/\(residenceId)/users/\(userId)/",
            token: token, responseType: TestMessageResponse.self
        )
        return result.succeeded
    }

    static func listResidenceUsers(token: String, residenceId: Int) -> [TestResidenceUser]? {
        return performRequest(
            method: "GET", path: "/residences/\(residenceId)/users/",
            token: token, responseType: [TestResidenceUser].self
        )
    }

    // MARK: - Raw Request (for custom/edge-case assertions)

    /// Make a raw request and return the full APIResult with status code.
    static func rawRequest(method: String, path: String, body: [String: Any]? = nil, token: String? = nil) -> APIResult<Data> {
        guard let url = URL(string: "\(baseURL)\(path)") else {
            return APIResult(data: nil, statusCode: 0, errorBody: "Invalid URL")
        }

        var request = URLRequest(url: url)
        request.httpMethod = method
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.timeoutInterval = 15

        if let token = token {
            request.setValue("Token \(token)", forHTTPHeaderField: "Authorization")
        }
        if let body = body {
            request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        }

        let semaphore = DispatchSemaphore(value: 0)
        var result = APIResult<Data>(data: nil, statusCode: 0, errorBody: "No response")

        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            defer { semaphore.signal() }
            if let error = error {
                result = APIResult(data: nil, statusCode: 0, errorBody: error.localizedDescription)
                return
            }
            let status = (response as? HTTPURLResponse)?.statusCode ?? 0
            let bodyStr = data.flatMap { String(data: $0, encoding: .utf8) }
            if (200...299).contains(status) {
                result = APIResult(data: data, statusCode: status, errorBody: nil)
            } else {
                result = APIResult(data: nil, statusCode: status, errorBody: bodyStr)
            }
        }
        task.resume()
        semaphore.wait()
        return result
    }

    // MARK: - Reachability

    static func isBackendReachable() -> Bool {
        let result = rawRequest(method: "POST", path: "/auth/login/", body: [:])
        // Any HTTP response (even 400) means the backend is up
        return result.statusCode > 0
    }

    // MARK: - Private Core

    /// Perform a request and return the decoded value, or nil on failure (logs errors).
    private static func performRequest<T: Decodable>(
        method: String,
        path: String,
        body: [String: Any]? = nil,
        token: String? = nil,
        responseType: T.Type
    ) -> T? {
        let result = performRequestWithResult(method: method, path: path, body: body, token: token, responseType: responseType)
        return result.data
    }

    /// Perform a request and return the full APIResult with status code.
    static func performRequestWithResult<T: Decodable>(
        method: String,
        path: String,
        body: [String: Any]? = nil,
        token: String? = nil,
        responseType: T.Type
    ) -> APIResult<T> {
        guard let url = URL(string: "\(baseURL)\(path)") else {
            return APIResult(data: nil, statusCode: 0, errorBody: "Invalid URL: \(baseURL)\(path)")
        }

        var request = URLRequest(url: url)
        request.httpMethod = method
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.timeoutInterval = 15

        if let token = token {
            request.setValue("Token \(token)", forHTTPHeaderField: "Authorization")
        }

        if let body = body {
            request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        }

        let semaphore = DispatchSemaphore(value: 0)
        var result = APIResult<T>(data: nil, statusCode: 0, errorBody: "No response")

        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            defer { semaphore.signal() }

            if let error = error {
                print("[TestAPI] \(method) \(path) error: \(error.localizedDescription)")
                result = APIResult(data: nil, statusCode: 0, errorBody: error.localizedDescription)
                return
            }

            let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0

            guard let data = data else {
                print("[TestAPI] \(method) \(path) no data (status \(statusCode))")
                result = APIResult(data: nil, statusCode: statusCode, errorBody: "No data")
                return
            }

            let bodyStr = String(data: data, encoding: .utf8) ?? "<binary>"

            guard (200...299).contains(statusCode) else {
                print("[TestAPI] \(method) \(path) status \(statusCode): \(bodyStr)")
                result = APIResult(data: nil, statusCode: statusCode, errorBody: bodyStr)
                return
            }

            do {
                let decoded = try JSONDecoder().decode(T.self, from: data)
                result = APIResult(data: decoded, statusCode: statusCode, errorBody: nil)
            } catch {
                print("[TestAPI] \(method) \(path) decode error: \(error)\nBody: \(bodyStr)")
                result = APIResult(data: nil, statusCode: statusCode, errorBody: "Decode error: \(error)")
            }
        }
        task.resume()
        let waitResult = semaphore.wait(timeout: .now() + 30)
        if waitResult == .timedOut {
            print("[TestAPI] \(method) \(path) TIMEOUT after 30s")
            task.cancel()
            return APIResult(data: nil, statusCode: 0, errorBody: "Request timed out after 30s")
        }

        return result
    }
}
