/*
 * Copyright 2014 Julien Viet
 *
 * Julien Viet licenses this file to you under the Apache License, version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 */
package io.termd.core.telnet;

import org.apache.commons.net.telnet.EchoOptionHandler;
import org.apache.commons.net.telnet.SimpleOptionHandler;
import org.apache.commons.net.telnet.SuppressGAOptionHandler;
import org.apache.commons.net.telnet.TelnetNotificationHandler;
import org.apache.commons.net.telnet.TelnetOptionHandler;
import org.apache.commons.net.telnet.WindowSizeOptionHandler;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

/**
 * See <a href="http://commons.apache.org/proper/commons-net/examples/telnet/TelnetClientExample.java>for more possibilities</a>
 *
 * @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
 */
public abstract class TelnetHandlerTest extends TelnetTestBase {

  private void testOptionValue(Supplier<TelnetHandler> factory, TelnetOptionHandler optionHandler) throws Exception {
    server.start(factory);
    client.setOptionHandler(optionHandler);
    client.connect("localhost", 4000);
    await();
  }

  @Test
  public void testRejectEcho() throws Exception {
    final AtomicReference<Boolean> serverValue = new AtomicReference<>();
    EchoOptionHandler optionHandler = new EchoOptionHandler(false, false, false, false);
    testOptionValue(() -> new TelnetHandler() {
      @Override
      protected void onOpen(TelnetConnection conn) {
        conn.writeWillOption(Option.ECHO);
      }
      @Override
      protected void onEcho(boolean echo) {
        serverValue.set(echo);
        testComplete();
      }
    }, optionHandler);
    assertEquals(false, serverValue.get());
    assertEquals(false, optionHandler.getAcceptRemote());
  }

  @Test
  public void testAcceptEcho() throws Exception {
    final AtomicReference<Boolean> serverValue = new AtomicReference<>();
    EchoOptionHandler optionHandler = new EchoOptionHandler(false, false, false, true);
    testOptionValue(() -> new TelnetHandler() {
      @Override
      protected void onOpen(TelnetConnection conn) {
        conn.writeWillOption(Option.ECHO);
      }
      @Override
      protected void onEcho(boolean echo) {
        serverValue.set(echo);
        testComplete();
      }
    }, optionHandler);
    assertEquals(true, serverValue.get());
    assertEquals(true, optionHandler.getAcceptRemote());
  }

  @Test
  public void testRejectSGA() throws Exception {
    final AtomicReference<Boolean> serverValue = new AtomicReference<>();
    SuppressGAOptionHandler optionHandler = new SuppressGAOptionHandler(false, false, false, false);
    testOptionValue(() -> new TelnetHandler() {
      @Override
      protected void onOpen(TelnetConnection conn) {
        conn.writeWillOption(Option.SGA);
      }
      @Override
      protected void onSGA(boolean sga) {
        serverValue.set(sga);
        testComplete();
      }
    }, optionHandler);
    assertEquals(false, serverValue.get());
    assertEquals(false, optionHandler.getAcceptRemote());
  }

  @Test
  public void testAcceptSGA() throws Exception {
    final AtomicReference<Boolean> serverValue = new AtomicReference<>();
    SuppressGAOptionHandler optionHandler = new SuppressGAOptionHandler(false, false, false, true);
    testOptionValue(() -> new TelnetHandler() {
      @Override
      protected void onOpen(TelnetConnection conn) {
        conn.writeWillOption(Option.SGA);
      }
      @Override
      protected void onSGA(boolean sga) {
        serverValue.set(sga);
        testComplete();
      }
    }, optionHandler);
    assertEquals(true, serverValue.get());
    assertEquals(true, optionHandler.getAcceptRemote());
  }

  @Test
  public void testRejectNAWS() throws Exception {
    final AtomicReference<Boolean> serverValue = new AtomicReference<>();
    WindowSizeOptionHandler optionHandler = new WindowSizeOptionHandler(20, 10, false, false, false, false);
    testOptionValue(new Supplier<TelnetHandler>() {
      @Override
      public TelnetHandler get() {
        return new TelnetHandler() {
          @Override
          protected void onOpen(TelnetConnection conn) {
            conn.writeDoOption(Option.NAWS);
          }
          @Override
          protected void onNAWS(boolean naws) {
            serverValue.set(naws);
            testComplete();
          }
          @Override
          protected void onSize(int width, int height) {
            super.onSize(width, height);
          }
        };
      }
    }, optionHandler);
    assertEquals(false, serverValue.get());
    assertEquals(false, optionHandler.getAcceptLocal());
  }

  @Test
  public void testAcceptNAWS() throws Exception {
    final AtomicReference<Boolean> serverValue = new AtomicReference<>();
    final AtomicReference<int[]> size = new AtomicReference<>();
    WindowSizeOptionHandler optionHandler = new WindowSizeOptionHandler(20, 10, false, false, true, false);
    testOptionValue(() -> new TelnetHandler() {
      @Override
      protected void onOpen(TelnetConnection conn) {
        conn.writeDoOption(Option.NAWS);
      }
      @Override
      protected void onNAWS(boolean naws) {
        serverValue.set(naws);
      }
      @Override
      protected void onSize(int width, int height) {
        size.set(new int[]{width, height});
        testComplete();
      }
    }, optionHandler);
    assertEquals(true, serverValue.get());
    assertEquals(true, optionHandler.getAcceptLocal());
    assertEquals(2, size.get().length);
    assertEquals(20, size.get()[0]);
    assertEquals(10, size.get()[1]);
  }

  @Test
  public void testOpen() throws Exception {
    server.start(() -> new TelnetHandler() {
      @Override
      protected void onOpen(TelnetConnection conn) {
        testComplete();
      }
    });
    client.connect("localhost", 4000);
    await();
  }

  @Test
  public void testClientDisconnect() throws Exception {
    server.start(() -> new TelnetHandler() {
      @Override
      protected void onClose() {
        testComplete();
      }
    });
    try {
      client.connect("localhost", 4000);
    } finally {
      client.disconnect();
    }
    await();
  }

  @Test
  public void testServerClose() throws Exception {
    server.start(() -> new TelnetHandler() {
      @Override
      protected void onOpen(TelnetConnection conn) {
        conn.close();
      }
      @Override
      protected void onClose() {
        testComplete();
      }
    });
    client.connect("localhost", 4000);
    await();
  }

  @Test
  public void testSend() throws Exception {
    server.start(() -> new TelnetHandler() {
      @Override
      protected void onOpen(TelnetConnection conn) {
        conn.write(new byte[]{0, 1, 2, 3, 127, (byte) 0x80, (byte) 0x81, -1});
      }
    });
    client.connect("localhost", 4000);
    byte[] data = client.assertReadBytes(8);
    assertEquals((byte)0, data[0]);
    assertEquals((byte)1, data[1]);
    assertEquals((byte)2, data[2]);
    assertEquals((byte)3, data[3]);
    assertEquals((byte)127, data[4]);
    assertEquals((byte)0, data[5]);
    assertEquals((byte)1, data[6]);
    assertEquals((byte)127, data[7]);
  }

  @Test
  public void testReceive() throws Exception {
    server.start(() -> new TelnetHandler() {
      @Override
      protected void onData(byte[] data) {
        assertEquals(7, data.length);
        assertEquals((byte) 0, data[0]);
        assertEquals((byte) 1, data[1]);
        assertEquals((byte) 2, data[2]);
        assertEquals((byte) 3, data[3]);
        assertEquals((byte) 127, data[4]);
        assertEquals((byte) 0x80, data[5]);
        assertEquals((byte) 0x81, data[6]);
        testComplete();
      }
    });
    client.connect("localhost", 4000).write(new byte[]{0, 1, 2, 3, 127, (byte) 0x80, (byte) 0x81}).flush();
    await();
  }

  @Test
  public void testWillUnknownOption() throws Exception {
    server.start(TelnetHandler::new);
    client.registerNotifHandler((negotiation_code, option_code) -> {
      if (option_code == 47) {
        assertEquals(TelnetNotificationHandler.RECEIVED_DONT, negotiation_code);
        testComplete();
      }
    });
    client.setOptionHandler(new SimpleOptionHandler(47, true, false, false, false));
    client.connect("localhost", 4000);
    await();
  }

  @Test
  public void testDoUnknownOption() throws Exception {
    server.start(TelnetHandler::new);
    client.registerNotifHandler((negotiation_code, option_code) -> {
      if (option_code == 47) {
        assertEquals(TelnetNotificationHandler.RECEIVED_WONT, negotiation_code);
        testComplete();
      }
    });
    client.setOptionHandler(new SimpleOptionHandler(47, false, true, false, false));
    client.connect("localhost", 4000);
    await();
  }

  @Test
  public void testReceiveBinary() throws Exception {
    final CountDownLatch latch = new CountDownLatch(1);
    server.start(() -> new TelnetHandler() {
      @Override
      protected void onOpen(TelnetConnection conn) {
        conn.writeDoOption(Option.BINARY);
      }

      @Override
      protected void onSendBinary(boolean binary) {
        if (binary) {
          fail("Was not expecting a do for binary option");
        }
      }

      @Override
      protected void onReceiveBinary(boolean binary) {
        if (binary) {
          latch.countDown();
        } else {
          fail("Was not expecting a won't for binary option");
        }
      }

      @Override
      protected void onData(byte[] data) {
        assertEquals(1, data.length);
        assertEquals((byte) -1, data[0]);
        testComplete();
      }
    });
    client.setOptionHandler(new SimpleOptionHandler(0, false, false, true, false));
    client.connect("localhost", 4000);
    latch.await();
    client.writeDirectAndFlush((byte)-1, (byte)-1);
    await();
  }

  @Test
  public void testSendBinary() throws Exception {
    final CountDownLatch latch = new CountDownLatch(1);
    server.start(new Supplier<TelnetHandler>() {
      @Override
      public TelnetHandler get() {
        return new TelnetHandler() {
          private TelnetConnection conn;

          @Override
          protected void onOpen(TelnetConnection conn) {
            this.conn = conn;
            conn.writeWillOption(Option.BINARY);
          }

          @Override
          protected void onSendBinary(boolean binary) {
            if (binary) {
              conn.write(new byte[]{'h', 'e', 'l', 'l', 'o', -1});
              latch.countDown();
            } else {
              fail("Was not expecting a don't for binary option");
            }
          }

          @Override
          protected void onReceiveBinary(boolean binary) {
            if (binary) {
              fail("Was not expecting a will for binary option");
            }
          }
        };
      }
    });
    client.setOptionHandler(new SimpleOptionHandler(0, false, false, false, true));
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    client.client.registerSpyStream(baos);
    client.connect("localhost", 4000);
    latch.await();
    Reader reader = new InputStreamReader(client.client.getInputStream());
    int expectedLen = 5;
    char[] hello = new char[expectedLen];
    int num = 0;
    while (num < expectedLen) {
      int read = reader.read(hello, num, expectedLen - num);
      if (read == -1) {
        fail("Unexpected");
      }
      num += read;
    }
    assertEquals(5, num);
    assertEquals("hello", new String(hello));
    long now = System.currentTimeMillis();
    byte[] data;
    while ((data = baos.toByteArray()).length < 10) {
      assertTrue(System.currentTimeMillis() - now < 10000);
      Thread.sleep(5);
    }
    assertEquals((byte)'h', data[3]);
    assertEquals((byte)'e', data[4]);
    assertEquals((byte)'l', data[5]);
    assertEquals((byte)'l', data[6]);
    assertEquals((byte)'o', data[7]);
    assertEquals((byte)-1, data[8]);
    assertEquals((byte)-1, data[9]);
  }
}
