import SwiftUI
import Testing

@testable import Recording

@Suite("Recording view model")
struct RecordingViewModelTests {

    typealias Model = RecordingView.Model

    // MARK: Initial state

    @MainActor
    @Suite("Initial state")
    struct InitialState {

        @Test func `initial state is not recording with a zeroed timer`() {
            let model = Model()

            #expect(model.state == .notRecording)
            #expect(model.elapsedSeconds == 0)
            #expect(model.textTimer == "00:00")
            #expect(model.transcription == nil)
        }

    }

    // MARK: Button presses

    @MainActor
    @Suite("Button presses")
    struct ButtonPresses {

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [Model.State.recording, .paused, .recording, .processing]
        ))
        func `pressed main transitions to the expected state`(
            from initial: Model.State,
            to expected: Model.State
        ) {
            let model = Model()

            model.state = initial

            model.pressedMain()

            #expect(model.state == expected)
        }

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [Model.State.notRecording, .recording, .processing, .processing]
        ))
        func `pressed send transitions to the expected state`(
            from initial: Model.State,
            to expected: Model.State
        ) {
            let model = Model()

            model.state = initial

            model.pressedSend()

            #expect(model.state == expected)
        }

    }

    // MARK: Computed properties

    @MainActor
    @Suite("Computed properties")
    struct ComputedProperties {

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [ImageResource.Icon.record, .Icon.pause, .Icon.record, .Icon.record]
        ))
        func `main icon matches the state`(
            for state: Model.State,
            expected: ImageResource
        ) {
            let model = Model()

            model.state = state

            #expect(model.iconMain == expected)
        }

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [nil, nil, ImageResource.Icon.send, nil]
        ))
        func `send icon matches the state`(
            for state: Model.State,
            expected: ImageResource?
        ) {
            let model = Model()

            model.state = state

            #expect(model.iconSend == expected)
        }

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [false, false, false, true]
        ))
        func `is processing only while processing`(
            for state: Model.State,
            expected: Bool
        ) {
            let model = Model()

            model.state = state

            #expect(model.isProcessing == expected)
        }

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [false, false, false, true]
        ))
        func `main button is disabled only while processing`(
            for state: Model.State,
            expected: Bool
        ) {
            let model = Model()

            model.state = state

            #expect(model.shouldDisableMain == expected)
        }

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [false, false, true, true]
        ))
        func `send button is visible while paused or processing`(
            for state: Model.State,
            expected: Bool
        ) {
            let model = Model()

            model.state = state

            #expect(model.shouldShowSend == expected)
        }

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [false, true, true, true]
        ))
        func `timer is visible unless not recording`(
            for state: Model.State,
            expected: Bool
        ) {
            let model = Model()

            model.state = state

            #expect(model.shouldShowTimer == expected)
        }

    }

    // MARK: Timer

    @MainActor
    @Suite("Timer")
    struct Timer {

        @Test func `ticks while recording`() async throws {
            let model = Model()

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            try await Task.sleep(for: .seconds(1.2))

            #expect(model.elapsedSeconds >= 1)
            #expect(model.textTimer == "00:01")

            model.state = .notRecording
            model.updatedState(shouldRestartTimer: false)
        }

        @Test func `stops while paused`() async throws {
            let model = Model()

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            try await Task.sleep(for: .seconds(1.2))

            model.state = .paused
            model.updatedState(shouldRestartTimer: false)

            let secondsWhenPaused = model.elapsedSeconds

            try await Task.sleep(for: .seconds(1.2))

            #expect(secondsWhenPaused >= 1)
            #expect(model.elapsedSeconds == secondsWhenPaused)
        }

        @Test func `keeps elapsed seconds when resumed`() async throws {
            let model = Model()

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            try await Task.sleep(for: .seconds(1.2))

            model.state = .paused
            model.updatedState(shouldRestartTimer: false)

            let secondsWhenPaused = model.elapsedSeconds

            model.state = .recording
            model.updatedState(shouldRestartTimer: false)

            #expect(model.elapsedSeconds == secondsWhenPaused)

            model.state = .notRecording
            model.updatedState(shouldRestartTimer: false)
        }

        @Test func `restarts for a new recording`() async throws {
            let model = Model()

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            try await Task.sleep(for: .seconds(1.2))

            model.state = .paused
            model.updatedState(shouldRestartTimer: false)

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            #expect(model.elapsedSeconds == 0)

            model.state = .notRecording
            model.updatedState(shouldRestartTimer: false)
        }

    }

    // MARK: Capturer

    @MainActor
    @Suite("Capturer")
    struct Capturer {

        @Test func `starts the capture for a new recording`() async throws {
            let capturer = CapturingMock()
            let model = Model(capturer: capturer)

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            try await Task.sleep(for: .seconds(0.1))

            #expect(capturer.countStart == 1)
            #expect(capturer.countResume == 0)

            model.state = .notRecording
            model.updatedState(shouldRestartTimer: false)
        }

        @Test func `pauses the capture while paused`() async throws {
            let capturer = CapturingMock()
            let model = Model(capturer: capturer)

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            model.state = .paused
            model.updatedState(shouldRestartTimer: false)

            try await Task.sleep(for: .seconds(0.1))

            #expect(capturer.countPause == 1)
        }

        @Test func `resumes the capture after a pause`() async throws {
            let capturer = CapturingMock()
            let model = Model(capturer: capturer)

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            model.state = .paused
            model.updatedState(shouldRestartTimer: false)

            model.state = .recording
            model.updatedState(shouldRestartTimer: false)

            try await Task.sleep(for: .seconds(0.1))

            #expect(capturer.countStart == 1)
            #expect(capturer.countResume == 1)

            model.state = .notRecording
            model.updatedState(shouldRestartTimer: false)
        }

        @Test func `stops the capture when the input is sent`() async throws {
            let capturer = CapturingMock()
            let model = Model(
                capturer: capturer,
                transcribe: TranscribingMock()
            )

            model.state = .processing
            model.updatedState(shouldRestartTimer: false)

            try await Task.sleep(for: .seconds(0.5))

            #expect(capturer.countStop == 1)
        }

        @Test func `falls back to not recording when the capture fails`() async throws {
            let capturer = CapturingMock()

            capturer.error = ErrorMock()

            let model = Model(capturer: capturer)

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            try await Task.sleep(for: .seconds(0.1))

            #expect(model.state == .notRecording)
        }

    }

    // MARK: Processing

    @MainActor
    @Suite("Processing")
    struct Processing {

        @Test func `returns to not recording with a reset timer and a transcription`() async throws {
            let model = Model(
                transcribe: TranscribingMock()
            )

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            try await Task.sleep(for: .seconds(1.2))

            model.state = .paused
            model.updatedState(shouldRestartTimer: false)

            #expect(model.elapsedSeconds >= 1)

            model.state = .processing
            model.updatedState(shouldRestartTimer: false)

            try await Task.sleep(for: .seconds(0.5))

            #expect(model.state == .notRecording)
            #expect(model.elapsedSeconds == 0)
            #expect(model.transcription != nil)
        }

        @Test func `clears the transcription when the transcriber fails`() async throws {
            let transcriber = TranscribingMock()

            transcriber.error = ErrorMock()

            let model = Model(
                transcribe: transcriber
            )

            model.state = .processing
            model.updatedState(shouldRestartTimer: false)

            try await Task.sleep(for: .seconds(0.5))

            #expect(model.state == .notRecording)
            #expect(model.elapsedSeconds == 0)
            #expect(model.transcription == nil)
        }

        @Test func `clears the transcription when a new recording starts`() async throws {
            let model = Model(
                transcribe: TranscribingMock()
            )

            model.state = .processing
            model.updatedState(shouldRestartTimer: false)

            try await Task.sleep(for: .seconds(0.5))

            #expect(model.transcription != nil)

            model.state = .recording
            model.updatedState(shouldRestartTimer: true)

            #expect(model.transcription == nil)

            model.state = .notRecording
            model.updatedState(shouldRestartTimer: false)
        }

    }

}

// MARK: - Mocks

/// A recording service that counts its invocations and can be configured to fail.
@MainActor
private final class CapturingMock: Capturing {

    /// The error the service throws from every method, or `nil` when it should succeed.
    var error: Error?

    /// The number of times ``start()`` has been called.
    private(set) var countStart = 0

    /// The number of times ``pause()`` has been called.
    private(set) var countPause = 0

    /// The number of times ``resume()`` has been called.
    private(set) var countResume = 0

    /// The number of times ``stop()`` has been called.
    private(set) var countStop = 0

    func start() async throws {
        countStart += 1

        try throwConfiguredError()
    }

    func pause() async throws {
        countPause += 1

        try throwConfiguredError()
    }

    func resume() async throws {
        countResume += 1

        try throwConfiguredError()
    }

    func stop() async throws -> Data {
        countStop += 1

        try throwConfiguredError()

        return .init()
    }

    private func throwConfiguredError() throws {
        if let error {
            throw error
        }
    }

}

/// A transcribing service with a configurable delay, output, and failure.
@MainActor
private final class TranscribingMock: Transcribing {

    /// The duration of the simulated transcription work.
    var delay: Duration = .seconds(0.2)

    /// The error the service throws, or `nil` when it should succeed.
    var error: Error?

    /// The transcription the service returns.
    var transcription = "This is a mocked transcription."

    func callAsFunction(_ audio: Data, locale: Locale) async throws -> Transcription {
        try await Task.sleep(for: delay)

        if let error {
            throw error
        }

        return .init(text: transcription)
    }

}

/// An error to configure the service mocks with.
private struct ErrorMock: Error {}
