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

package org.postgresql.test.jdbc3;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.postgresql.core.ServerVersion;
import org.postgresql.test.TestUtil;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Jdbc3BlobTest {
  private static final String TABLE = "blobtest";
  private static final String INSERT = "INSERT INTO " + TABLE + " VALUES (1, lo_creat(-1))";
  private static final String SELECT = "SELECT ID, DATA FROM " + TABLE + " WHERE ID = 1";

  private Connection conn;

  @BeforeAll
  static void createTables() throws Exception {
    try (Connection conn = TestUtil.openDB()) {
      TestUtil.createTable(conn, TABLE, "ID INT PRIMARY KEY, DATA OID");
    }
  }

  @AfterAll
  static void dropTables() throws Exception {
    try (Connection conn = TestUtil.openDB()) {
      TestUtil.dropTable(conn, TABLE);
    }
  }

  @BeforeEach
  void setUp() throws Exception {
    conn = TestUtil.openDB();
    conn.setAutoCommit(false);
  }

  @AfterEach
  void tearDown() throws SQLException {
    conn.setAutoCommit(true);
    try {
      Statement stmt = conn.createStatement();
      try {
        stmt.execute("SELECT lo_unlink(DATA) FROM " + TABLE);
      } finally {
        try {
          stmt.close();
        } catch (Exception e) {
        }
      }
    } finally {
      try {
        TestUtil.execute(conn, "TRUNCATE " + TABLE);
      } finally {
        TestUtil.closeDB(conn);
      }
    }
  }

  /**
   * Test the writing and reading of a single byte.
   */
  @Test
  void test1Byte() throws SQLException {
    byte[] data = {(byte) 'a'};
    readWrite(data);
  }

  /**
   * Test the writing and reading of a few bytes.
   */
  @Test
  void manyBytes() throws SQLException {
    byte[] data = "aaaaaaaaaa".getBytes();
    readWrite(data);
  }

  /**
   * Test writing a single byte with an offset.
   */
  @Test
  void test1ByteOffset() throws SQLException {
    byte[] data = {(byte) 'a'};
    readWrite(10, data);
  }

  /**
   * Test the writing and reading of a few bytes with an offset.
   */
  @Test
  void manyBytesOffset() throws SQLException {
    byte[] data = "aaaaaaaaaa".getBytes();
    readWrite(10, data);
  }

  /**
   * Tests all of the byte values from 0 - 255.
   */
  @Test
  void allBytes() throws SQLException {
    byte[] data = new byte[256];
    for (int i = 0; i < data.length; i++) {
      data[i] = (byte) i;
    }
    readWrite(data);
  }

  @Test
  void truncate() throws SQLException {
    if (!TestUtil.haveMinimumServerVersion(conn, ServerVersion.v8_3)) {
      return;
    }

    byte[] data = new byte[100];
    for (byte i = 0; i < data.length; i++) {
      data[i] = i;
    }
    readWrite(data);

    PreparedStatement ps = conn.prepareStatement(SELECT);
    ResultSet rs = ps.executeQuery();

    assertTrue(rs.next());
    Blob blob = rs.getBlob("DATA");

    assertEquals(100, blob.length());

    blob.truncate(50);
    assertEquals(50, blob.length());

    blob.truncate(150);
    assertEquals(150, blob.length());

    data = blob.getBytes(1, 200);
    assertEquals(150, data.length);
    for (byte i = 0; i < 50; i++) {
      assertEquals(i, data[i]);
    }

    for (int i = 50; i < 150; i++) {
      assertEquals(0, data[i]);
    }
  }

  /**
   *
   * @param data data to write
   * @throws SQLException if something goes wrong
   */
  public void readWrite(byte[] data) throws SQLException {
    readWrite(1, data);
  }

  /**
   *
   * @param offset data offset
   * @param data data to write
   * @throws SQLException if something goes wrong
   */
  public void readWrite(int offset, byte[] data) throws SQLException {
    PreparedStatement ps = conn.prepareStatement(INSERT);
    ps.executeUpdate();
    ps.close();

    ps = conn.prepareStatement(SELECT);
    ResultSet rs = ps.executeQuery();

    assertTrue(rs.next());
    Blob b = rs.getBlob("DATA");
    b.setBytes(offset, data);

    rs.close();
    ps.close();

    ps = conn.prepareStatement(SELECT);
    rs = ps.executeQuery();

    assertTrue(rs.next());
    b = rs.getBlob("DATA");
    byte[] rspData = b.getBytes(offset, data.length);
    assertArrayEquals(data, rspData, "Request should be the same as the response");

    rs.close();
    ps.close();
  }

  /**
   * Test the writing and reading of a single byte.
   */
  @Test
  void test1ByteStream() throws SQLException, IOException {
    byte[] data = {(byte) 'a'};
    readWriteStream(data);
  }

  /**
   * Test the writing and reading of a few bytes.
   */
  @Test
  void manyBytesStream() throws SQLException, IOException {
    byte[] data = "aaaaaaaaaa".getBytes();
    readWriteStream(data);
  }

  /**
   * Test writing a single byte with an offset.
   */
  @Test
  void test1ByteOffsetStream() throws SQLException, IOException {
    byte[] data = {(byte) 'a'};
    readWriteStream(10, data);
  }

  /**
   * Test the writing and reading of a few bytes with an offset.
   */
  @Test
  void manyBytesOffsetStream() throws SQLException, IOException {
    byte[] data = "aaaaaaaaaa".getBytes();
    readWriteStream(10, data);
  }

  /**
   * Tests all of the byte values from 0 - 255.
   */
  @Test
  void allBytesStream() throws SQLException, IOException {
    byte[] data = new byte[256];
    for (int i = 0; i < data.length; i++) {
      data[i] = (byte) i;
    }
    readWriteStream(data);
  }

  public void readWriteStream(byte[] data) throws SQLException, IOException {
    readWriteStream(1, data);
  }

  /**
   * Reads then writes data to the blob via a stream.
   */
  public void readWriteStream(int offset, byte[] data) throws SQLException, IOException {
    PreparedStatement ps = conn.prepareStatement(INSERT);
    ps.executeUpdate();
    ps.close();

    ps = conn.prepareStatement(SELECT);
    ResultSet rs = ps.executeQuery();

    assertTrue(rs.next());
    Blob b = rs.getBlob("DATA");
    OutputStream out = b.setBinaryStream(offset);
    out.write(data);
    out.flush();
    out.close();

    rs.close();
    ps.close();

    ps = conn.prepareStatement(SELECT);
    rs = ps.executeQuery();

    assertTrue(rs.next());
    b = rs.getBlob("DATA");
    InputStream in = b.getBinaryStream();
    byte[] rspData = new byte[data.length];
    in.skip(offset - 1);
    in.read(rspData);
    in.close();

    assertArrayEquals(data, rspData, "Request should be the same as the response");

    rs.close();
    ps.close();
  }

  @Test
  void pattern() throws SQLException {
    byte[] data = "abcdefghijklmnopqrstuvwxyz0123456789".getBytes();
    byte[] pattern = "def".getBytes();

    PreparedStatement ps = conn.prepareStatement(INSERT);
    ps.executeUpdate();
    ps.close();

    ps = conn.prepareStatement(SELECT);
    ResultSet rs = ps.executeQuery();

    assertTrue(rs.next());
    Blob b = rs.getBlob("DATA");
    b.setBytes(1, data);

    rs.close();
    ps.close();

    ps = conn.prepareStatement(SELECT);
    rs = ps.executeQuery();

    assertTrue(rs.next());
    b = rs.getBlob("DATA");
    long position = b.position(pattern, 1);
    byte[] rspData = b.getBytes(position, pattern.length);
    assertArrayEquals(pattern, rspData, "Request should be the same as the response");

    rs.close();
    ps.close();
  }
}
