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.error == nil)
            #expect(model.textAlertMessage == nil)
            #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.drive(to: 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.drive(to: initial)

            model.pressedSend()

            #expect(model.state == expected)
        }

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

            model.drive(to: initial)

            model.pressedDiscard()

            #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.drive(to: 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.drive(to: 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.drive(to: state)

            #expect(model.isProcessing == expected)
        }

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [
                LocalizedStringResource.viewRecordingButtonMainLabelRecord,
                .viewRecordingButtonMainLabelPause,
                .viewRecordingButtonMainLabelResume,
                .viewRecordingButtonMainLabelRecord,
            ]
        ))
        func `main label matches the state`(
            for state: Model.State,
            expected: LocalizedStringResource
        ) {
            let model = Model()

            model.drive(to: state)

            #expect(model.labelMain == expected)
        }

        @Test(arguments: zip(
            [Model.State.notRecording, .recording, .paused, .processing],
            [
                LocalizedStringResource.viewRecordingButtonSendLabelSend,
                .viewRecordingButtonSendLabelSend,
                .viewRecordingButtonSendLabelSend,
                .viewRecordingButtonSendLabelProcessing,
            ]
        ))
        func `send label matches the state`(
            for state: Model.State,
            expected: LocalizedStringResource
        ) {
            let model = Model()

            model.drive(to: state)

            #expect(model.labelSend == 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.drive(to: state)

            #expect(model.shouldDisableMain == expected)
        }

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

            model.drive(to: state)

            #expect(model.shouldShowActions == 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.drive(to: state)

            #expect(model.shouldShowActions == 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.drive(to: state)

            #expect(model.shouldShowTimer == expected)
        }

    }

    // MARK: Timer

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

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

            model.pressedMain()

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

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

            model.pressedMain()
        }

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

            model.pressedMain()

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

            model.pressedMain()

            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.pressedMain()

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

            model.pressedMain()

            let secondsWhenPaused = model.elapsedSeconds

            model.pressedMain()

            #expect(model.elapsedSeconds == secondsWhenPaused)

            model.pressedMain()
        }

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

            model.pressedMain()

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

            model.pressedMain()
            model.pressedSend()

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

            #expect(model.state == .notRecording)

            model.pressedMain()

            #expect(model.elapsedSeconds == 0)

            model.pressedMain()
        }

    }

    // 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.pressedMain()

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

            #expect(capturer.calls == ["start"])

            model.pressedMain()
        }

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

            model.pressedMain()
            model.pressedMain()

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

            #expect(capturer.calls == ["start", "pause"])
        }

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

            model.pressedMain()
            model.pressedMain()
            model.pressedMain()

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

            #expect(capturer.calls == ["start", "pause", "resume"])

            model.pressedMain()
        }

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

            model.drive(to: .processing)

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

            #expect(capturer.calls == ["start", "pause", "stop"])
        }

        @Test
        func `stops the capture when a paused recording is discarded`() async throws {
            let capturer = CapturingMock()
            let model = Model(capturer: capturer)

            model.drive(to: .paused)

            model.pressedDiscard()

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

            #expect(capturer.calls == ["start", "pause", "stop"])
            #expect(model.state == .notRecording)
            #expect(model.elapsedSeconds == 0)
        }

        @Test
        func `serializes the calls when states change rapidly`() async throws {
            let capturer = CapturingMock()

            capturer.delay = .seconds(0.2)

            let model = Model(capturer: capturer)

            model.pressedMain()
            model.pressedMain()
            model.pressedMain()

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

            #expect(capturer.calls == ["start"])

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

            #expect(capturer.calls == ["start", "pause", "resume"])

            model.pressedMain()
        }

        @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.pressedMain()

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

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

    }

    // MARK: Errors

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

        @Test
        func `surfaces a capture failure`() async throws {
            let capturer = CapturingMock()

            capturer.error = ErrorMock()

            let model = Model(capturer: capturer)

            model.pressedMain()

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

            #expect(model.error == .captureFailed)
            #expect(model.state == .notRecording)
            #expect(model.textAlertMessage == .viewRecordingAlertErrorMessageCapture)
        }

        @Test
        func `surfaces a denied microphone permission`() async throws {
            let capturer = CapturingMock()

            capturer.error = AudioCapturingError.permissionNotGranted

            let model = Model(capturer: capturer)

            model.pressedMain()

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

            #expect(model.error == .permissionDenied)
            #expect(model.textAlertMessage == .viewRecordingAlertErrorMessagePermission)
        }

        @Test
        func `surfaces unavailable speech model assets`() async throws {
            let transcriber = TranscribingMock()

            transcriber.error = AudioTranscribingError.assetsNotInstalled

            let model = Model(
                transcribe: transcriber
            )

            model.drive(to: .processing)

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

            #expect(model.error == .assetsUnavailable)
            #expect(model.textAlertMessage == .viewRecordingAlertErrorMessageAssets)
            #expect(model.transcription == nil)
        }

        @Test
        func `surfaces a transcription failure`() async throws {
            let transcriber = TranscribingMock()

            transcriber.error = ErrorMock()

            let model = Model(
                transcribe: transcriber
            )

            model.drive(to: .processing)

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

            #expect(model.error == .transcriptionFailed)
            #expect(model.textAlertMessage == .viewRecordingAlertErrorMessageTranscription)
            #expect(model.transcription == nil)
        }

        @Test
        func `clears the error when dismissed`() async throws {
            let capturer = CapturingMock()

            capturer.error = ErrorMock()

            let model = Model(capturer: capturer)

            model.pressedMain()

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

            #expect(model.error != nil)

            model.dismissedError()

            #expect(model.error == nil)
            #expect(model.textAlertMessage == nil)
        }

        @Test
        func `clears the error when a new recording starts`() async throws {
            let capturer = CapturingMock()

            capturer.error = ErrorMock()

            let model = Model(capturer: capturer)

            model.pressedMain()

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

            #expect(model.error != nil)

            capturer.error = nil
            model.pressedMain()

            #expect(model.error == nil)

            model.pressedMain()
        }

    }

    // MARK: Interruptions

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

        @Test
        func `pauses an ongoing recording when the capture is interrupted`() async throws {
            let capturer = CapturingMock()
            let model = Model(capturer: capturer)

            model.pressedMain()

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

            capturer.interrupt()

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

            #expect(model.state == .paused)
            #expect(capturer.calls == ["start", "pause"])
        }

        @Test
        func `changes nothing when the capture is interrupted while paused`() async throws {
            let capturer = CapturingMock()
            let model = Model(capturer: capturer)

            model.pressedMain()
            model.pressedMain()

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

            capturer.interrupt()

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

            #expect(model.state == .paused)
            #expect(capturer.calls == ["start", "pause"])
        }

        @Test
        func `resumes an interrupted recording when the interruption ends with the resume hint`() async throws {
            let capturer = CapturingMock()
            let model = Model(capturer: capturer)

            model.pressedMain()

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

            capturer.interrupt()

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

            capturer.endInterruption(shouldResume: true)

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

            #expect(model.state == .recording)
            #expect(capturer.calls == ["start", "pause", "resume"])

            model.pressedMain()
        }

        @Test
        func `stays paused when the interruption ends without the resume hint`() async throws {
            let capturer = CapturingMock()
            let model = Model(capturer: capturer)

            model.pressedMain()

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

            capturer.interrupt()

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

            capturer.endInterruption(shouldResume: false)

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

            #expect(model.state == .paused)
            #expect(capturer.calls == ["start", "pause"])
        }

        @Test
        func `never resumes a recording the user paused`() async throws {
            let capturer = CapturingMock()
            let model = Model(capturer: capturer)

            model.pressedMain()
            model.pressedMain()

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

            capturer.endInterruption(shouldResume: true)

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

            #expect(model.state == .paused)
            #expect(capturer.calls == ["start", "pause"])
        }

    }

    // 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.pressedMain()

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

            model.pressedMain()

            #expect(model.elapsedSeconds >= 1)

            model.pressedSend()

            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.drive(to: .processing)

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

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

        @Test
        func `discarding while processing cancels the transcription`() async throws {
            let capturer = CapturingMock()
            let transcriber = TranscribingMock()

            transcriber.delay = .seconds(0.5)

            let model = Model(
                capturer: capturer,
                transcribe: transcriber
            )

            model.drive(to: .processing)

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

            model.pressedDiscard()

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

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

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

            model.drive(to: .processing)

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

            #expect(model.transcription != nil)

            model.pressedMain()

            #expect(model.transcription == nil)

            model.pressedMain()
        }

    }

}

// MARK: - Helpers

private extension RecordingView.Model {

    /// Drives the model from its initial state to the given state through its press handlers, as the view's buttons would.
    ///
    /// - Parameter state: The state to drive the model to.
    func drive(
        to state: State
    ) {
        switch state {
        case .notRecording:
            break
        case .recording:
            pressedMain()
        case .paused:
            pressedMain()
            pressedMain()
        case .processing:
            pressedMain()
            pressedMain()
            pressedSend()
        }
    }

}
