package ctbrec.recorder;

import static java.time.temporal.ChronoUnit.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;

import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.Settings;
import ctbrec.recorder.download.Download;

class RecordingPreconditionsTest {

    private Config config;
    private Settings settings = new Settings();

    @BeforeEach
    void setup() {
        config = mock(Config.class);
        when(config.getSettings()).thenReturn(settings);
        try (MockedStatic<Config> staticConfigMock = mockStatic(Config.class)) {
            staticConfigMock.when(Config::getInstance).thenReturn(config);
        }
    }

    @Test
    void testRecorderNotInRecordingMode() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        Model model = mock(Model.class);

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
        assertEquals("Recorder is not in recording mode", ex.getMessage());
    }

    @Test
    void testModelIsSuspended() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        Model model = mock(Model.class);

        when(recorder.isRecording()).thenReturn(true);
        when(model.isSuspended()).thenReturn(true);
        when(model.toString()).thenReturn("Mockita Boobilicious");

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
        assertEquals("Recording for model Mockita Boobilicious is suspended", ex.getMessage());
    }

    @Test
    void testModelMarkedForLaterRecording() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        Model model = mock(Model.class);

        when(recorder.isRecording()).thenReturn(true);
        when(model.isMarkedForLaterRecording()).thenReturn(true);
        when(model.toString()).thenReturn("Mockita Boobilicious");

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
        assertEquals("Model Mockita Boobilicious is marked for later recording", ex.getMessage());
    }

    @Test
    void testRecordUntil() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        Model model = mock(Model.class);

        when(recorder.isRecording()).thenReturn(true);
        when(model.getRecordUntil()).thenReturn(Instant.now().minus(1, HOURS));
        when(model.toString()).thenReturn("Mockita Boobilicious");

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
        assertTrue(ex.getMessage().contains("Recording expired at "));
    }

    @Test
    void testRecordingAlreadyRunning() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        Model model = mock(Model.class);

        when(recorder.isRecording()).thenReturn(true);
        Map<Model, Recording> recordingProcesses = new HashMap<>();
        recordingProcesses.put(model, new Recording());
        when(recorder.getRecordingProcesses()).thenReturn(recordingProcesses);
        when(model.getRecordUntil()).thenReturn(Instant.MAX);
        when(model.toString()).thenReturn("Mockita Boobilicious");

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
        assertEquals("A recording for model Mockita Boobilicious is already running", ex.getMessage());
    }

    @Test
    void testModelShouldBeRecorded() throws InvalidKeyException, NoSuchAlgorithmException, IOException, ExecutionException, InterruptedException {
        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        Model model = mock(Model.class);

        when(recorder.isRecording()).thenReturn(true);
        List<Model> modelsToRecord = new LinkedList<>();
        when(recorder.getModels()).thenReturn(modelsToRecord);
        when(model.getRecordUntil()).thenReturn(Instant.MAX);
        when(model.toString()).thenReturn("Mockita Boobilicious");
        when(model.isOnline(true)).thenReturn(true);

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
        assertEquals("Model Mockita Boobilicious has been removed. Restarting of recording cancelled.", ex.getMessage());

        modelsToRecord.add(model);
        reset(recorder);
        when(recorder.isRecording()).thenReturn(true);
        when(recorder.getModels()).thenReturn(modelsToRecord);
        when(recorder.enoughSpaceForRecording()).thenReturn(true);
        assertDoesNotThrow(() -> preconditions.check(model, config));
    }

    @Test
    void testEnoughSpace() throws InvalidKeyException, NoSuchAlgorithmException, IOException, ExecutionException, InterruptedException {
        Model model = mock(Model.class);
        when(model.getRecordUntil()).thenReturn(Instant.MAX);
        when(model.toString()).thenReturn("Mockita Boobilicious");
        when(model.isOnline(true)).thenReturn(true);

        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        List<Model> modelsToRecord = new LinkedList<>();
        modelsToRecord.add(model);
        when(recorder.isRecording()).thenReturn(true);
        when(recorder.getModels()).thenReturn(modelsToRecord);
        when(recorder.enoughSpaceForRecording()).thenReturn(false);

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(model, config));
        assertEquals("Not enough disk space for recording", ex.getMessage());
    }

    @Test
    void testNoOtherFromGroupIsRecording() throws InvalidKeyException, NoSuchAlgorithmException, IOException, ExecutionException, InterruptedException {
        Model mockita = mock(Model.class);
        when(mockita.getRecordUntil()).thenReturn(Instant.MAX);
        when(mockita.toString()).thenReturn("Mockita Boobilicious");
        when(mockita.isOnline(true)).thenReturn(true);
        when(mockita.getUrl()).thenReturn("http://localhost/mockita");

        Model theOtherOne = mock(Model.class);
        when(theOtherOne.getRecordUntil()).thenReturn(Instant.MAX);
        when(theOtherOne.toString()).thenReturn("The Other One");
        when(theOtherOne.isOnline(true)).thenReturn(true);
        when(theOtherOne.getUrl()).thenReturn("http://localhost/theOtherOne");

        ModelGroup group = new ModelGroup();
        group.add(theOtherOne);
        group.add(mockita);

        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        List<Model> modelsToRecord = new LinkedList<>();
        settings.models = modelsToRecord;
        modelsToRecord.add(theOtherOne);
        modelsToRecord.add(mockita);
        when(recorder.isRecording()).thenReturn(true);
        when(recorder.getModels()).thenReturn(modelsToRecord);
        when(recorder.enoughSpaceForRecording()).thenReturn(true);
        when(recorder.getModelGroup(theOtherOne)).thenReturn(Optional.of(group));
        when(recorder.getModelGroup(mockita)).thenReturn(Optional.of(group));
        Map<Model, Recording> recordingProcesses = new HashMap<>();
        recordingProcesses.put(theOtherOne, new Recording());
        when(recorder.getRecordingProcesses()).thenReturn(recordingProcesses);


        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
        assertEquals("The Other One from the same group is already recorded", ex.getMessage());
    }

    @Test
    void testModelIsOffline() throws InvalidKeyException, NoSuchAlgorithmException, IOException, ExecutionException, InterruptedException {
        Model mockita = mock(Model.class);
        when(mockita.getRecordUntil()).thenReturn(Instant.MAX);
        when(mockita.getName()).thenReturn("Mockita Boobilicious");
        when(mockita.toString()).thenReturn("Mockita Boobilicious");
        when(mockita.isOnline(true)).thenReturn(false);

        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        List<Model> modelsToRecord = new LinkedList<>();
        settings.models = modelsToRecord;
        modelsToRecord.add(mockita);
        when(recorder.isRecording()).thenReturn(true);
        when(recorder.getModels()).thenReturn(modelsToRecord);
        when(recorder.enoughSpaceForRecording()).thenReturn(true);

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
        assertEquals("Mockita Boobilicious's room is not public", ex.getMessage());
    }

    @Test
    void testModelIsOnlineWithExpection() throws InvalidKeyException, NoSuchAlgorithmException, IOException, ExecutionException, InterruptedException {
        Model mockita = mock(Model.class);
        when(mockita.isOnline(true)).thenThrow(new IOException("Service unavailable"));
        when(mockita.getRecordUntil()).thenReturn(Instant.MAX);
        when(mockita.getName()).thenReturn("Mockita Boobilicious");

        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        List<Model> modelsToRecord = new LinkedList<>();
        settings.models = modelsToRecord;
        modelsToRecord.add(mockita);
        when(recorder.isRecording()).thenReturn(true);
        when(recorder.getModels()).thenReturn(modelsToRecord);
        when(recorder.enoughSpaceForRecording()).thenReturn(true);

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
        assertEquals("Mockita Boobilicious's room is not public", ex.getMessage());

        reset(mockita);
        when(mockita.isOnline(true)).thenThrow(new InterruptedException());
        when(mockita.getRecordUntil()).thenReturn(Instant.MAX);
        when(mockita.getName()).thenReturn("Mockita Boobilicious");
        ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
        assertEquals("Mockita Boobilicious's room is not public", ex.getMessage());
    }

    @Test
    void testDownloadSlotsExhausted() throws InvalidKeyException, NoSuchAlgorithmException, IOException, ExecutionException, InterruptedException {
        settings.concurrentRecordings = 1;

        Model mockita = mock(Model.class);
        when(mockita.isOnline(true)).thenReturn(true);
        when(mockita.getRecordUntil()).thenReturn(Instant.MAX);
        when(mockita.getName()).thenReturn("Mockita Boobilicious");

        Model theOtherOne = mock(Model.class);
        when(theOtherOne.getRecordUntil()).thenReturn(Instant.MAX);
        when(theOtherOne.toString()).thenReturn("The Other One");
        when(theOtherOne.isOnline(true)).thenReturn(true);
        when(theOtherOne.getUrl()).thenReturn("http://localhost/theOtherOne");

        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        List<Model> modelsToRecord = new LinkedList<>();
        settings.models = modelsToRecord;
        modelsToRecord.add(mockita);
        when(recorder.isRecording()).thenReturn(true);
        when(recorder.getModels()).thenReturn(modelsToRecord);
        when(recorder.enoughSpaceForRecording()).thenReturn(true);
        Map<Model, Recording> recordingProcesses = new HashMap<>();
        recordingProcesses.put(theOtherOne, new Recording());
        when(recorder.getRecordingProcesses()).thenReturn(recordingProcesses);

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        PreconditionNotMetException ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
        assertEquals("Other models have higher prio, not starting recording for Mockita Boobilicious", ex.getMessage());

        settings.concurrentRecordings = -1;
        ex = assertThrows(PreconditionNotMetException.class, () -> preconditions.check(mockita, config));
        assertEquals("Other models have higher prio, not starting recording for Mockita Boobilicious", ex.getMessage());

        settings.concurrentRecordings = 0;
        assertDoesNotThrow(() -> preconditions.check(mockita, config));

        settings.concurrentRecordings = 1;
        recordingProcesses.clear();
        assertDoesNotThrow(() -> preconditions.check(mockita, config));
    }

    @Test
    void testDownloadSlotFreedUp() throws InvalidKeyException, NoSuchAlgorithmException, IOException, ExecutionException, InterruptedException {
        settings.concurrentRecordings = 1;

        Model mockita = mock(Model.class);
        when(mockita.isOnline(true)).thenReturn(true);
        when(mockita.getRecordUntil()).thenReturn(Instant.MAX);
        when(mockita.getName()).thenReturn("Mockita Boobilicious");
        when(mockita.getPriority()).thenReturn(100);

        NextGenLocalRecorder recorder = mock(NextGenLocalRecorder.class);
        List<Model> modelsToRecord = new LinkedList<>();
        settings.models = modelsToRecord;
        modelsToRecord.add(mockita);
        when(recorder.isRecording()).thenReturn(true);
        when(recorder.getModels()).thenReturn(modelsToRecord);
        when(recorder.enoughSpaceForRecording()).thenReturn(true);
        Map<Model, Recording> recordingProcesses = new HashMap<>();
        when(recorder.getRecordingProcesses()).thenReturn(recordingProcesses);

        Model theOtherOne = mock(Model.class);
        when(theOtherOne.getRecordUntil()).thenReturn(Instant.MAX);
        when(theOtherOne.toString()).thenReturn("The Other One");
        when(theOtherOne.isOnline(true)).thenReturn(true);
        when(theOtherOne.getUrl()).thenReturn("http://localhost/theOtherOne");
        when(theOtherOne.getPriority()).thenReturn(50);
        recordingProcesses.put(theOtherOne, mockRecordingProcess(theOtherOne));

        Model lowestPrio = mock(Model.class);
        when(lowestPrio.getRecordUntil()).thenReturn(Instant.MAX);
        when(lowestPrio.toString()).thenReturn("Lowest");
        when(lowestPrio.isOnline(true)).thenReturn(true);
        when(lowestPrio.getUrl()).thenReturn("http://localhost/lowest");
        when(lowestPrio.getPriority()).thenReturn(1);
        recordingProcesses.put(theOtherOne, mockRecordingProcess(lowestPrio));

        RecordingPreconditions preconditions = new RecordingPreconditions(recorder);
        assertDoesNotThrow(() -> preconditions.check(mockita, config));
        verify(recorder).stopRecordingProcess(lowestPrio);
    }

    private Recording mockRecordingProcess(Model model) {
        Download download = mock(Download.class);
        when(download.getModel()).thenReturn(model);
        Recording runningRecording = mock(Recording.class);
        when(runningRecording.getDownload()).thenReturn(download);
        return runningRecording;
    }
}
