/*
 * Copyright (c) 2016, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.replication;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assume.assumeThat;

import org.postgresql.PGConnection;
import org.postgresql.PGProperty;
import org.postgresql.core.BaseConnection;
import org.postgresql.core.ServerVersion;
import org.postgresql.test.TestUtil;
import org.postgresql.test.util.rules.ServerVersionRule;
import org.postgresql.test.util.rules.annotation.HaveMinimalServerVersion;

import org.hamcrest.CoreMatchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.nio.ByteBuffer;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Properties;

@HaveMinimalServerVersion("9.4")
public class PhysicalReplicationTest {

  private static final String SLOT_NAME = "pgjdbc_physical_replication_slot";

  @Rule
  public ServerVersionRule versionRule = new ServerVersionRule();

  private Connection replConnection;
  private Connection sqlConnection;

  @Before
  public void setUp() throws Exception {
    sqlConnection = TestUtil.openDB();
    //DriverManager.setLogWriter(new PrintWriter(System.out));
    replConnection = openReplicationConnection();
    TestUtil.createTable(sqlConnection, "test_physic_table",
        "pk serial primary key, name varchar(100)");
    TestUtil.recreatePhysicalReplicationSlot(sqlConnection, SLOT_NAME);
  }

  @After
  public void tearDown() throws Exception {
    replConnection.close();
    TestUtil.dropTable(sqlConnection, "test_physic_table");
    TestUtil.dropReplicationSlot(sqlConnection, SLOT_NAME);
    sqlConnection.close();
  }

  @Test
  public void testReceiveChangesWithoutReplicationSlot() throws Exception {
    PGConnection pgConnection = (PGConnection) replConnection;

    LogSequenceNumber lsn = getCurrentLSN();

    Statement st = sqlConnection.createStatement();
    st.execute("insert into test_physic_table(name) values('previous value')");
    st.close();

    PGReplicationStream stream =
        pgConnection
            .getReplicationAPI()
            .replicationStream()
            .physical()
            .withStartPosition(lsn)
            .start();

    ByteBuffer read = stream.read();

    assertThat("Physical replication can be start without replication slot",
        read, CoreMatchers.notNullValue()
    );
  }

  @Test
  public void testReceiveChangesWithReplicationSlot() throws Exception {
    PGConnection pgConnection = (PGConnection) replConnection;

    LogSequenceNumber lsn = getCurrentLSN();

    Statement st = sqlConnection.createStatement();
    st.execute("insert into test_physic_table(name) values('previous value')");
    st.close();

    PGReplicationStream stream =
        pgConnection
            .getReplicationAPI()
            .replicationStream()
            .physical()
            .withSlotName(SLOT_NAME)
            .withStartPosition(lsn)
            .start();

    ByteBuffer read = stream.read();

    assertThat(read, CoreMatchers.notNullValue());
  }

  @Test
  public void testAfterStartStreamingDBSlotStatusActive() throws Exception {
    PGConnection pgConnection = (PGConnection) replConnection;

    LogSequenceNumber lsn = getCurrentLSN();

    PGReplicationStream stream =
        pgConnection
            .getReplicationAPI()
            .replicationStream()
            .physical()
            .withSlotName(SLOT_NAME)
            .withStartPosition(lsn)
            .start();

    boolean isActive = isActiveOnView();
    stream.close();

    Assert.assertThat(
        "After start streaming, database status should be update on view pg_replication_slots to active",
        isActive, equalTo(true)
    );
  }

  @Test
  public void testAfterCloseReplicationStreamDBSlotStatusNotActive() throws Exception {
    PGConnection pgConnection = (PGConnection) replConnection;

    LogSequenceNumber lsn = getCurrentLSN();

    PGReplicationStream stream =
        pgConnection
            .getReplicationAPI()
            .replicationStream()
            .physical()
            .withSlotName(SLOT_NAME)
            .withStartPosition(lsn)
            .start();

    boolean isActive = isActiveOnView();
    assumeThat(isActive, equalTo(true));

    stream.close();

    isActive = isActiveOnView();
    Assert.assertThat(
        "Execute close method on PGREplicationStream should lead to stop replication, "
            + "as result we wait that on view pg_replication_slots status for slot will change to no active",
        isActive, equalTo(false)
    );
  }

  @Test
  public void testWalRecordCanBeRepeatBeRestartReplication() throws Exception {
    PGConnection pgConnection = (PGConnection) replConnection;

    LogSequenceNumber lsn = getCurrentLSN();

    Statement st = sqlConnection.createStatement();
    st.execute("insert into test_physic_table(name) values('previous value')");
    st.close();

    PGReplicationStream stream =
        pgConnection
            .getReplicationAPI()
            .replicationStream()
            .physical()
            .withSlotName(SLOT_NAME)
            .withStartPosition(lsn)
            .start();

    byte[] first = toByteArray(stream.read());
    stream.close();

    //reopen stream
    stream =
        pgConnection
            .getReplicationAPI()
            .replicationStream()
            .physical()
            .withSlotName(SLOT_NAME)
            .withStartPosition(lsn)
            .start();

    byte[] second = toByteArray(stream.read());
    stream.close();

    boolean arrayEquals = Arrays.equals(first, second);
    assertThat("On same replication connection we can restart replication from already "
            + "received LSN if they not recycled yet on backend",
        arrayEquals, CoreMatchers.equalTo(true)
    );
  }

  @Test
  public void restartPhysicalReplicationWithoutRepeatMessage() throws Exception {
    PGConnection pgConnection = (PGConnection) replConnection;

    LogSequenceNumber lsn = getCurrentLSN();

    Statement st = sqlConnection.createStatement();
    st.execute("insert into test_physic_table(name) values('first value')");
    st.close();

    PGReplicationStream stream =
        pgConnection
            .getReplicationAPI()
            .replicationStream()
            .physical()
            .withSlotName(SLOT_NAME)
            .withStartPosition(lsn)
            .start();

    byte[] streamOneFirstPart = toByteArray(stream.read());
    LogSequenceNumber restartLSN = stream.getLastReceiveLSN();

    st = sqlConnection.createStatement();
    st.execute("insert into test_physic_table(name) values('second value')");
    st.close();

    byte[] streamOneSecondPart = toByteArray(stream.read());
    stream.close();

    //reopen stream
    stream =
        pgConnection
            .getReplicationAPI()
            .replicationStream()
            .physical()
            .withSlotName(SLOT_NAME)
            .withStartPosition(restartLSN)
            .start();

    byte[] streamTwoFirstPart = toByteArray(stream.read());
    stream.close();

    boolean arrayEquals = Arrays.equals(streamOneSecondPart, streamTwoFirstPart);
    assertThat("Interrupt physical replication and restart from lastReceiveLSN should not "
            + "lead to repeat messages skip part of them",
        arrayEquals, CoreMatchers.equalTo(true)
    );
  }

  private boolean isActiveOnView() throws SQLException {
    boolean result = false;
    Statement st = sqlConnection.createStatement();
    ResultSet
        rs =
        st.executeQuery("select * from pg_replication_slots where slot_name = '" + SLOT_NAME + "'");
    if (rs.next()) {
      result = rs.getBoolean("active");
    }
    rs.close();
    st.close();
    return result;
  }

  private byte[] toByteArray(ByteBuffer buffer) {
    int offset = buffer.arrayOffset();
    byte[] source = buffer.array();
    return Arrays.copyOfRange(source, offset, source.length);
  }

  private LogSequenceNumber getCurrentLSN() throws SQLException {
    Statement st = sqlConnection.createStatement();
    ResultSet rs = null;
    try {
      rs = st.executeQuery("select "
          + (((BaseConnection) sqlConnection).haveMinimumServerVersion(ServerVersion.v10)
          ? "pg_current_wal_lsn()" : "pg_current_xlog_location()"));

      if (rs.next()) {
        String lsn = rs.getString(1);
        return LogSequenceNumber.valueOf(lsn);
      } else {
        return LogSequenceNumber.INVALID_LSN;
      }
    } finally {
      if (rs != null) {
        rs.close();
      }
      st.close();
    }
  }

  private Connection openReplicationConnection() throws Exception {
    Properties properties = new Properties();
    PGProperty.ASSUME_MIN_SERVER_VERSION.set(properties, "9.4");
    PGProperty.REPLICATION.set(properties, "database");
    return TestUtil.openDB(properties);
  }
}
