/*
 * Copyright (C) 2012 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.crsh.telnet.term;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import test.shell.base.BaseProcess;
import test.shell.base.BaseProcessFactory;
import test.shell.base.BaseShell;
import org.crsh.cli.impl.completion.CompletionMatch;
import org.crsh.cli.impl.Delimiter;
import org.crsh.cli.spi.Completion;
import org.crsh.telnet.term.processor.Processor;
import org.crsh.shell.Shell;
import org.crsh.shell.ShellProcess;
import org.crsh.shell.ShellProcessContext;
import org.crsh.shell.ShellResponse;
import org.crsh.telnet.term.spi.TestTermIO;
import org.crsh.telnet.term.console.ConsoleTerm;

import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class ProcessorTestCase extends TestCase {

  public void testLine() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO));
    controller.assertStart();

    //
    controller.connector.append("abc\r\n");
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertChars("b").assertFlush();
    controller.connector.assertChars("c").assertFlush();
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("abc").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  public void testDel() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO));
    controller.assertStart();

    //
    controller.connector.append("abc");
    controller.connector.appendDel();
    controller.connector.append("\r\n");
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertChars("b").assertFlush();
    controller.connector.assertChars("c").assertFlush();
    controller.connector.assertDel().assertFlush();
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("ab").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  public void testBreak() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO));
    controller.assertStart();

    //
    controller.connector.append("abc");
    controller.connector.appendBreak();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertChars("b").assertFlush();
    controller.connector.assertChars("c").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.connector.append("def\r\n");
    controller.connector.assertChars("d").assertFlush();
    controller.connector.assertChars("e").assertFlush();
    controller.connector.assertChars("f").assertFlush();
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("def").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  public void testInsert() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO));
    controller.assertStart();

    //
    controller.connector.append("ab");
    controller.connector.appendMoveLeft();
    controller.connector.append("c\r\n");
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertChars("b").assertFlush();
    controller.connector.assertMoveLeft().assertFlush();
    controller.connector.assertChars("cb").assertMoveLeft().assertFlush();
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("acb").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  public void testIdempotentMoveRight() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO));
    controller.assertStart();

    //
    controller.connector.append("a");
    controller.connector.appendMoveRight();
    controller.connector.append("\r\n");
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  public void testIdempotentMoveLeft() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO));
    controller.assertStart();

    //
    controller.connector.appendMoveLeft();
    controller.connector.append("a");
    controller.connector.append("\r\n");
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  public void testMove() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO));
    controller.assertStart();

    //
    controller.connector.append("a");
    controller.connector.append("\r\n");
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.connector.appendMoveUp();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.append("\r\n");
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.connector.appendMoveUp();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.appendMoveDown();
    controller.connector.assertDel().assertFlush();

    //
    controller.assertStop();
  }

  public void testIdempotentMoveUp() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO));
    controller.assertStart();

    //
    controller.connector.append("a");
    controller.connector.append("\r\n");
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.connector.appendMoveUp();
    controller.connector.appendMoveUp();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.append("\r\n");
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  public void testIdempotentMoveDown() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO));
    controller.assertStart();

    //
    controller.connector.append("a");
    controller.connector.append("\r\n");
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.connector.appendMoveDown();
    controller.connector.append("\r\n");
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  public void testCompletion1() throws Exception {
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO) {
      @Override
      public CompletionMatch complete(String prefix) {
        return new CompletionMatch(Delimiter.EMPTY, Completion.create(new StringBuilder(prefix).reverse().toString(), false));
      }
    });
    controller.assertStart();

    //
    controller.connector.append("ab");
    controller.connector.appendTab();
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertChars("b").assertFlush();
    controller.connector.assertChars("ba").assertFlush();
  }

  public void testMultiLine() throws Exception {
    final LinkedList<String> requests = new LinkedList<String>();
    final CountDownLatch latch = new CountDownLatch(1);
    Controller controller = create(new BaseShell(BaseProcessFactory.ECHO) {
      @Override
      public ShellProcess createProcess(String request) {
        return new BaseProcess(request) {
          @Override
          protected ShellResponse execute(String request) {
            requests.add(request);
            latch.countDown();
            return super.execute(request);
          }
        };
      }
    });
    controller.assertStart();

    //
    controller.connector.append("a\\\r\n");
    controller.connector.assertChars("a").assertFlush();
    controller.connector.assertChars("\\").assertFlush();
    controller.connector.assertCRLF().assertFlush().assertChars("> ").assertFlush();
    assertEquals(Collections.<String>emptyList(), requests);
    controller.connector.append("b\r\n");
    controller.connector.assertChars("b").assertFlush();
    controller.connector.assertCRLF().assertFlush();
    latch.await(5, TimeUnit.SECONDS);
    assertEquals(Collections.singletonList("ab"), requests);
  }

  public void testCLS() throws Exception {
    Controller controller = create(new BaseShell(new BaseProcessFactory() {
      @Override
      public BaseProcess create(String request) {
        return new BaseProcess(request) {
          @Override
          public void process(String request, ShellProcessContext processContext) throws IOException {
            if ("bye".equals(request)) {
              processContext.end(ShellResponse.close());
            } else {
              processContext.cls();
              processContext.end(ShellResponse.ok());
            }
          }
        };
      }
    }));

    //
    controller.assertStart();
    controller.connector.append("\r\n");
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertCLS().assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  public void testFlush() throws Exception {
    Controller controller = create(new BaseShell(new BaseProcessFactory() {
      @Override
      public BaseProcess create(String request) {
        return new BaseProcess(request) {
          @Override
          public void process(String request, ShellProcessContext processContext) throws IOException {
            if ("bye".equals(request)) {
              processContext.end(ShellResponse.close());
            } else {
              processContext.flush();
              processContext.end(ShellResponse.ok());
            }
          }
        };
      }
    }));

    //
    controller.assertStart();
    controller.connector.append("\r\n");
    controller.connector.assertCRLF().assertFlush();
    controller.connector.assertFlush(); // The good one
    controller.connector.assertFlush();
    controller.connector.assertCRLF().assertChars("% ").assertFlush();

    //
    controller.assertStop();
  }

  private Controller create(Shell shell) throws IOException {
    return new Controller(new TestTermIO(), shell);
  }

  private class Controller implements Runnable {

    /** . */
    private volatile boolean running;

    /** . */
    private final CountDownLatch startSync;

    /** . */
    private final CountDownLatch stopSync;

    /** . */
    private final Thread thread;

    /** . */
    private final TestTermIO connector;

    /** . */
    private final Processor processor;

    private Controller(TestTermIO connector, Shell shell) {
      this.running = true;
      this.startSync = new CountDownLatch(1);
      this.stopSync = new CountDownLatch(1);
      this.thread = new Thread(this);
      this.connector = connector;
      this.processor = new Processor(new ConsoleTerm(connector), shell);
    }

    public void assertStart() {
      thread.start();

      //
      try {
        assertTrue(startSync.await(1, TimeUnit.SECONDS));
      }
      catch (InterruptedException e) {
        AssertionFailedError afe = new AssertionFailedError();
        afe.initCause(e);
        throw afe;
      }

      //
      assertTrue(running);

      //
      connector.assertCRLF();
      connector.assertChars("% ");
      connector.assertFlush();
    }

    public void run() {
      running = true;
      startSync.countDown();
      try {
        processor.run();
      }
      finally {
        running = false;
        stopSync.countDown();
      }
    }

    public void assertStop() {
      assertTrue(running);

      //
      connector.append("bye\r\n");
      connector.assertChars("b").assertFlush();
      connector.assertChars("y").assertFlush();
      connector.assertChars("e").assertFlush();
      connector.assertCRLF().assertFlush();

      //
      try {
        assertTrue(stopSync.await(4, TimeUnit.SECONDS));
      }
      catch (InterruptedException e) {
        AssertionFailedError afe = new AssertionFailedError();
        afe.initCause(e);
        throw afe;
      }

      //
      connector.assertEmpty();

      //
      assertFalse(running);
    }
  }
}
