//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

import Foundation
import Testing

@testable import ContainerizationOS

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif

@Suite("BidirectionalRelay tests")
final class BidirectionalRelayTests {

    /// Creates a Unix domain socket pair and returns (fd0, fd1).
    private func makeSocketPair() throws -> (Int32, Int32) {
        var fds: [Int32] = [0, 0]
        #if os(macOS)
        let result = socketpair(AF_UNIX, SOCK_STREAM, 0, &fds)
        #else
        let result = socketpair(AF_UNIX, Int32(SOCK_STREAM.rawValue), 0, &fds)
        #endif
        try #require(result == 0, "socketpair should succeed, errno: \(errno)")
        return (fds[0], fds[1])
    }

    /// Writes all bytes to a file descriptor, retrying on partial writes.
    private func writeAll(fd: Int32, data: [UInt8]) throws {
        var offset = 0
        while offset < data.count {
            let n = data.withUnsafeBufferPointer { buf in
                write(fd, buf.baseAddress!.advanced(by: offset), data.count - offset)
            }
            try #require(n > 0, "write failed, errno: \(errno)")
            offset += n
        }
    }

    /// Reads exactly `count` bytes from a file descriptor with a timeout.
    /// Returns the data read, or nil if the timeout expires.
    private func readWithTimeout(fd: Int32, count: Int, timeoutSeconds: Double) -> [UInt8]? {
        // Set fd to non-blocking for poll-based reading.
        let flags = fcntl(fd, F_GETFL)
        _ = fcntl(fd, F_SETFL, flags | O_NONBLOCK)
        defer { _ = fcntl(fd, F_SETFL, flags) }

        var result = [UInt8](repeating: 0, count: count)
        var totalRead = 0
        let deadline = Date().addingTimeInterval(timeoutSeconds)

        while totalRead < count && Date() < deadline {
            let n = result.withUnsafeMutableBufferPointer { buf in
                read(fd, buf.baseAddress!.advanced(by: totalRead), count - totalRead)
            }
            if n > 0 {
                totalRead += n
            } else if n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK) {
                // Not ready yet, brief sleep before retry.
                usleep(1000)  // 1ms
            } else {
                break
            }
        }
        return totalRead == count ? result : nil
    }

    /// Sets a small send buffer on a socket to make it fill quickly.
    private func setSendBufferSize(fd: Int32, size: Int32) {
        var bufSize = size
        setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &bufSize, socklen_t(MemoryLayout<Int32>.size))
    }

    // MARK: - Test 1: Basic relay

    @Test
    func testBasicRelay() throws {
        // Create two socketpairs:
        //   pair1: (a0) --- relay ---> (b0)
        //   pair2: (a1) <-- relay --- (b1)
        // The relay connects a1 <-> b0.
        // Write to a0, read from b1 (data flows: a0 → a1 → relay → b0 → b1).
        let (a0, a1) = try makeSocketPair()
        let (b0, b1) = try makeSocketPair()
        defer {
            close(a0)
            close(b1)
        }

        let relay = BidirectionalRelay(fd1: a1, fd2: b0)
        try relay.start()

        let testData: [UInt8] = Array("Hello, BidirectionalRelay!".utf8)
        try writeAll(fd: a0, data: testData)

        let received = readWithTimeout(fd: b1, count: testData.count, timeoutSeconds: 2.0)
        #expect(received == testData, "Data should pass through the relay")

        // Test the reverse direction: write to b1, read from a0.
        let reverseData: [UInt8] = Array("Reverse direction".utf8)
        try writeAll(fd: b1, data: reverseData)

        let reverseReceived = readWithTimeout(fd: a0, count: reverseData.count, timeoutSeconds: 2.0)
        #expect(reverseReceived == reverseData, "Data should flow in reverse through the relay")

        relay.stop()
    }

    // MARK: - Test 2: Cross-connection head-of-line blocking

    @Test
    func testNoCrossConnectionBlocking() throws {
        // Two relays sharing a single serial queue (simulating the old architecture).
        // One relay's destination is saturated (not drained).
        // The other relay should still work — proving per-connection isolation.
        let sharedQueue = DispatchQueue(label: "test.shared-queue")

        // Relay 1: a0 → a1 --relay1--> b0 → b1 (b1 won't be read, causing backpressure)
        let (a0, a1) = try makeSocketPair()
        let (b0, b1) = try makeSocketPair()

        // Relay 2: c0 → c1 --relay2--> d0 → d1 (d1 will be read normally)
        let (c0, c1) = try makeSocketPair()
        let (d0, d1) = try makeSocketPair()

        defer {
            close(a0)
            close(b1)
            close(c0)
            close(d1)
        }

        // Shrink send buffers to make them fill quickly.
        setSendBufferSize(fd: b0, size: 4096)

        let relay1 = BidirectionalRelay(fd1: a1, fd2: b0, queue: sharedQueue)
        let relay2 = BidirectionalRelay(fd1: c1, fd2: d0, queue: sharedQueue)

        try relay1.start()
        try relay2.start()

        // Saturate relay1: write data into a0 but never read from b1.
        // This fills b0's send buffer, triggering backpressure on relay1.
        let largeData = [UInt8](repeating: 0x41, count: 65536)
        // Use non-blocking write to a0 so we don't block this test thread.
        let a0flags = fcntl(a0, F_GETFL)
        _ = fcntl(a0, F_SETFL, a0flags | O_NONBLOCK)
        _ = largeData.withUnsafeBufferPointer { buf in
            write(a0, buf.baseAddress!, buf.count)
        }
        _ = fcntl(a0, F_SETFL, a0flags)  // restore

        // Give relay1 time to process and get blocked.
        usleep(100_000)  // 100ms

        // Now test relay2: it should still work despite relay1 being backpressured.
        let testData: [UInt8] = Array("relay2 works!".utf8)
        try writeAll(fd: c0, data: testData)

        let received = readWithTimeout(fd: d1, count: testData.count, timeoutSeconds: 2.0)
        #expect(received != nil, "Relay2 should not be blocked by Relay1's backpressure")
        if let received {
            #expect(received == testData, "Relay2 data should be correct")
        }

        relay1.stop()
        relay2.stop()
    }

    // MARK: - Test 3: Backpressure recovery

    @Test
    func testBackpressureRecovery() throws {
        let (a0, a1) = try makeSocketPair()
        let (b0, b1) = try makeSocketPair()
        defer {
            close(a0)
            close(b1)
        }

        // Shrink b0's send buffer so backpressure kicks in quickly.
        setSendBufferSize(fd: b0, size: 4096)

        let relay = BidirectionalRelay(fd1: a1, fd2: b0)
        try relay.start()

        // Write enough data to trigger backpressure (more than the send buffer).
        let totalBytes = 32768
        let sendData = [UInt8]((0..<totalBytes).map { UInt8($0 & 0xFF) })

        // Write in a background thread since it may partially block.
        let writeThread = Thread {
            var offset = 0
            while offset < sendData.count {
                let n = sendData.withUnsafeBufferPointer { buf in
                    write(a0, buf.baseAddress!.advanced(by: offset), min(4096, sendData.count - offset))
                }
                if n > 0 {
                    offset += n
                } else if n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK) {
                    usleep(1000)
                } else {
                    break
                }
            }
        }
        writeThread.start()

        // Read from b1 (drain) — this should relieve backpressure.
        var received = [UInt8]()
        let readBuf = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: 4096)
        defer { readBuf.deallocate() }

        let deadline = Date().addingTimeInterval(5.0)
        let b1flags = fcntl(b1, F_GETFL)
        _ = fcntl(b1, F_SETFL, b1flags | O_NONBLOCK)

        while received.count < totalBytes && Date() < deadline {
            let n = read(b1, readBuf.baseAddress!, readBuf.count)
            if n > 0 {
                received.append(contentsOf: UnsafeBufferPointer(start: readBuf.baseAddress!, count: n))
            } else if n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK) {
                usleep(1000)
            } else {
                break
            }
        }

        #expect(received.count == totalBytes, "All bytes should be received after backpressure recovery (got \(received.count)/\(totalBytes))")
        #expect(received == sendData, "Received data should match sent data")

        relay.stop()
    }

    // MARK: - Test 4: EOF handling

    @Test
    func testEOFHandling() async throws {
        let (a0, a1) = try makeSocketPair()
        let (b0, b1) = try makeSocketPair()

        let relay = BidirectionalRelay(fd1: a1, fd2: b0)
        try relay.start()

        // Write some data, then close one end.
        let testData: [UInt8] = Array("goodbye".utf8)
        try writeAll(fd: a0, data: testData)
        close(a0)

        // Read the data from the other end.
        let received = readWithTimeout(fd: b1, count: testData.count, timeoutSeconds: 2.0)
        #expect(received == testData, "Data should arrive before EOF")

        // Close b1 as well so both directions see EOF.
        // (a0 closed → a1 reads EOF → source1 done;
        //  b1 closed → b0 reads EOF → source2 done → relay complete)
        close(b1)

        // The relay should detect EOF on both directions and complete.
        let completed = await withTaskGroup(of: Bool.self) { group in
            group.addTask {
                await relay.waitForCompletion()
                return true
            }
            group.addTask {
                try? await Task.sleep(nanoseconds: 3_000_000_000)  // 3 seconds
                return false
            }
            let result = await group.next()!
            group.cancelAll()
            return result
        }

        #expect(completed, "Relay should complete after both sides reach EOF")
    }

    // MARK: - Test 5: Stop while under backpressure

    @Test
    func testStopWhileBackpressured() async throws {
        // Verify that stop() works correctly when a read source is suspended
        // due to backpressure. Previously, cancelling a suspended dispatch source
        // would never deliver the cancel handler, leaking file descriptors.
        let (a0, a1) = try makeSocketPair()
        let (b0, b1) = try makeSocketPair()

        // Shrink b0's send buffer so backpressure kicks in quickly.
        setSendBufferSize(fd: b0, size: 4096)

        let relay = BidirectionalRelay(fd1: a1, fd2: b0)
        try relay.start()

        // Write enough to trigger backpressure but don't read from b1.
        let largeData = [UInt8](repeating: 0x42, count: 65536)
        let a0flags = fcntl(a0, F_GETFL)
        _ = fcntl(a0, F_SETFL, a0flags | O_NONBLOCK)
        _ = largeData.withUnsafeBufferPointer { buf in
            write(a0, buf.baseAddress!, buf.count)
        }

        // Give relay time to enter backpressure state (readSource suspended).
        usleep(100_000)  // 100ms

        // Stop the relay while it's backpressured. This should not hang or leak.
        relay.stop()

        // Close the external ends — the relay's fds should already be closed by stop().
        close(a0)
        close(b1)

        // The relay should complete (cancel handlers should have fired).
        let completed = await withTaskGroup(of: Bool.self) { group in
            group.addTask {
                await relay.waitForCompletion()
                return true
            }
            group.addTask {
                try? await Task.sleep(nanoseconds: 3_000_000_000)  // 3 seconds
                return false
            }
            let result = await group.next()!
            group.cancelAll()
            return result
        }

        #expect(completed, "Relay should complete after stop() even when backpressured")
    }
}
