/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.plugin.gdb.server;

import com.google.common.collect.ImmutableMap;

import org.eclipse.che.api.debugger.server.Debugger;
import org.eclipse.che.api.debugger.server.exceptions.DebuggerException;
import org.eclipse.che.api.debug.shared.model.Breakpoint;
import org.eclipse.che.api.debug.shared.model.DebuggerInfo;
import org.eclipse.che.api.debug.shared.model.Location;
import org.eclipse.che.api.debug.shared.model.StackFrameDump;
import org.eclipse.che.api.debug.shared.model.SimpleValue;
import org.eclipse.che.api.debug.shared.model.Variable;
import org.eclipse.che.api.debug.shared.model.VariablePath;
import org.eclipse.che.api.debug.shared.model.event.BreakpointActivatedEvent;
import org.eclipse.che.api.debug.shared.model.event.DebuggerEvent;
import org.eclipse.che.api.debug.shared.model.event.DisconnectEvent;
import org.eclipse.che.api.debug.shared.model.event.SuspendEvent;
import org.eclipse.che.api.debug.shared.model.impl.BreakpointImpl;
import org.eclipse.che.api.debug.shared.model.impl.LocationImpl;
import org.eclipse.che.api.debug.shared.model.impl.VariableImpl;
import org.eclipse.che.api.debug.shared.model.impl.VariablePathImpl;
import org.eclipse.che.api.debug.shared.model.impl.action.ResumeActionImpl;
import org.eclipse.che.api.debug.shared.model.impl.action.StartActionImpl;
import org.eclipse.che.api.debug.shared.model.impl.action.StepOutActionImpl;
import org.eclipse.che.api.debug.shared.model.impl.action.StepOverActionImpl;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

/**
 * @author Anatoliy Bazko
 */
public class GdbDebuggerTest {

    private String                       file;
    private Path                         sourceDirectory;
    private GdbServer                    gdbServer;
    private Debugger                     gdbDebugger;
    private BlockingQueue<DebuggerEvent> events;

    @BeforeClass
    public void beforeClass() throws Exception {
        file = GdbTest.class.getResource("/hello").getFile();
        sourceDirectory = Paths.get(GdbTest.class.getResource("/h.cpp").getFile());
        events = new ArrayBlockingQueue<>(10);
    }

    @BeforeMethod
    public void setUp() throws Exception {
        gdbServer = GdbServer.start("localhost", 1111, file);
    }

    @AfterMethod
    public void tearDown() throws Exception {
        gdbServer.stop();
    }

    @Test
    public void testDebugger() throws Exception {
        initializeDebugger();
        addBreakpoint();
        startDebugger();
        doSetAndGetValues();
//        stepInto();
        stepOver();
        stepOut();
        resume();
        deleteAllBreakpoints();
        disconnect();
    }

    private void deleteAllBreakpoints() throws DebuggerException {
        List<Breakpoint> breakpoints = gdbDebugger.getAllBreakpoints();
        assertEquals(breakpoints.size(), 1);

        gdbDebugger.deleteAllBreakpoints();

        breakpoints = gdbDebugger.getAllBreakpoints();
        assertTrue(breakpoints.isEmpty());
    }

    private void resume() throws DebuggerException, InterruptedException {
        gdbDebugger.resume(new ResumeActionImpl());

        DebuggerEvent debuggerEvent = events.take();
        assertTrue(debuggerEvent instanceof SuspendEvent);

        SuspendEvent suspendEvent = (SuspendEvent)debuggerEvent;
        assertEquals(suspendEvent.getLocation().getTarget(), "h.cpp");
        assertEquals(suspendEvent.getLocation().getLineNumber(), 7);
    }

    private void stepOut() throws DebuggerException, InterruptedException {
        try {
            gdbDebugger.stepOut(new StepOutActionImpl());
        } catch (DebuggerException e) {
            // ignore
        }

        DebuggerEvent debuggerEvent = events.take();
        assertTrue(debuggerEvent instanceof SuspendEvent);
    }

    private void stepOver() throws DebuggerException, InterruptedException {
        gdbDebugger.stepOver(new StepOverActionImpl());

        DebuggerEvent debuggerEvent = events.take();
        assertTrue(debuggerEvent instanceof SuspendEvent);

        SuspendEvent suspendEvent = (SuspendEvent)debuggerEvent;
        assertEquals(suspendEvent.getLocation().getTarget(), "h.cpp");
        assertEquals(suspendEvent.getLocation().getLineNumber(), 5);

        gdbDebugger.stepOver(new StepOverActionImpl());

        debuggerEvent = events.take();
        assertTrue(debuggerEvent instanceof SuspendEvent);

        suspendEvent = (SuspendEvent)debuggerEvent;
        assertEquals(suspendEvent.getLocation().getTarget(), "h.cpp");
        assertEquals(suspendEvent.getLocation().getLineNumber(), 6);

        gdbDebugger.stepOver(new StepOverActionImpl());

        debuggerEvent = events.take();
        assertTrue(debuggerEvent instanceof SuspendEvent);

        suspendEvent = (SuspendEvent)debuggerEvent;
        assertEquals(suspendEvent.getLocation().getTarget(), "h.cpp");
        assertEquals(suspendEvent.getLocation().getLineNumber(), 7);
    }

    private void doSetAndGetValues() throws DebuggerException {
        VariablePath variablePath = new VariablePathImpl("i");
        Variable variable = new VariableImpl("int", "i", "2", true, variablePath, Collections.emptyList(), false);

        SimpleValue value = gdbDebugger.getValue(variablePath);
        assertEquals(value.getValue(), "0");

        gdbDebugger.setValue(variable);

        value = gdbDebugger.getValue(variablePath);

        assertEquals(value.getValue(), "2");

        String expression = gdbDebugger.evaluate("i");
        assertEquals(expression, "2");

        expression = gdbDebugger.evaluate("10 + 10");
        assertEquals(expression, "20");

        StackFrameDump stackFrameDump = gdbDebugger.dumpStackFrame();
        assertTrue(stackFrameDump.getFields().isEmpty());
        assertEquals(stackFrameDump.getVariables().size(), 1);
        assertEquals(stackFrameDump.getVariables().get(0).getName(), "i");
        assertEquals(stackFrameDump.getVariables().get(0).getValue(), "2");
        assertEquals(stackFrameDump.getVariables().get(0).getType(), "int");
    }

    private void startDebugger() throws DebuggerException, InterruptedException {
        gdbDebugger.start(new StartActionImpl(Collections.emptyList()));

        assertEquals(events.size(), 1);

        DebuggerEvent debuggerEvent = events.take();
        assertTrue(debuggerEvent instanceof SuspendEvent);

        SuspendEvent suspendEvent = (SuspendEvent)debuggerEvent;
        assertEquals(suspendEvent.getLocation().getTarget(), "h.cpp");
        assertEquals(suspendEvent.getLocation().getLineNumber(), 7);
    }

    private void disconnect() throws DebuggerException, InterruptedException {
        gdbDebugger.disconnect();

        assertEquals(events.size(), 1);

        DebuggerEvent debuggerEvent = events.take();
        assertTrue(debuggerEvent instanceof DisconnectEvent);
    }

    private void addBreakpoint() throws DebuggerException, InterruptedException {
        Location location = new LocationImpl("h.cpp", 7);
        Breakpoint breakpoint = new BreakpointImpl(location);

        gdbDebugger.addBreakpoint(breakpoint);

        assertEquals(events.size(), 1);

        DebuggerEvent debuggerEvent = events.take();
        assertTrue(debuggerEvent instanceof BreakpointActivatedEvent);

        BreakpointActivatedEvent breakpointActivatedEvent = (BreakpointActivatedEvent)debuggerEvent;
        assertEquals(breakpointActivatedEvent.getBreakpoint().getLocation().getTarget(), "h.cpp");
        assertEquals(breakpointActivatedEvent.getBreakpoint().getLocation().getLineNumber(), 7);
    }

    private void initializeDebugger() throws DebuggerException {
        Map<String, String> properties = ImmutableMap.of("host", "localhost",
                                                         "port", "1111",
                                                         "binary", file,
                                                         "sources", sourceDirectory.getParent().toString());

        GdbDebuggerFactory gdbDebuggerFactory = new GdbDebuggerFactory();
        gdbDebugger = gdbDebuggerFactory.create(properties, events::add);


        DebuggerInfo debuggerInfo = gdbDebugger.getInfo();

        assertEquals(debuggerInfo.getFile(), file);
        assertEquals(debuggerInfo.getHost(), "localhost");
        assertEquals(debuggerInfo.getPort(), 1111);
        assertNotNull(debuggerInfo.getName());
        assertNotNull(debuggerInfo.getVersion());
    }
}