// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2012-2014 Monty Program Ab
// Copyright (c) 2015-2021 MariaDB Corporation Ab

package org.mariadb.jdbc.integration;

import static org.junit.jupiter.api.Assertions.*;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Clob;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.junit.jupiter.api.Test;
import org.mariadb.jdbc.Common;
import org.mariadb.jdbc.MariaDbClob;

public class ClobTest extends Common {

  private final byte[] bytes = "abc£de🙏fgh".getBytes(StandardCharsets.UTF_8);

  @Test
  public void length() {
    MariaDbClob clob = new MariaDbClob(bytes);
    assertEquals(11, clob.length());

    MariaDbClob clob2 = new MariaDbClob(bytes, 2, 3);
    assertEquals(2, clob2.length());
  }

  @Test
  public void getSubString() throws SQLException {
    MariaDbClob clob = new MariaDbClob(bytes);
    assertEquals("abc£de🙏", clob.getSubString(1, 8));
    assertEquals("abc£de🙏fgh", clob.getSubString(1, 21));
    assertEquals("abc£de🙏fgh", clob.getSubString(1, (int) clob.length()));
    assertEquals("ab", clob.getSubString(1, 2));
    assertEquals("🙏", clob.getSubString(7, 2));

    MariaDbClob clob2 = new MariaDbClob(bytes, 6, 6);

    assertEquals("e🙏f", clob2.getSubString(1, 20));
    assertEquals("🙏f", clob2.getSubString(2, 3));

    try {
      clob2.getSubString(0, 3);
      fail("must have thrown exception, min pos is 1");
    } catch (SQLException sqle) {
      // normal exception
    }
  }

  @Test
  public void getCharacterStream() throws SQLException {
    MariaDbClob clob = new MariaDbClob(bytes);
    assureReaderEqual("abc£de🙏", clob.getCharacterStream(1, 8));
    assureReaderEqual("abc£de🙏fgh", clob.getCharacterStream(1, 11));
    try {
      assureReaderEqual("abc£de🙏fgh", clob.getCharacterStream(1, 20));
      fail("must have throw exception, length > to number of characters");
    } catch (SQLException sqle) {
      // normal error
    }
    assureReaderEqual("bc£de🙏", clob.getCharacterStream(2, 7));

    MariaDbClob clob2 = new MariaDbClob(bytes, 2, 9);
    assureReaderEqual("c£de🙏", clob2.getCharacterStream(1, 6));
    try {
      assureReaderEqual("c£de🙏fg", clob2.getCharacterStream(1, 20));
      fail("must have throw exception, length > to number of characters");
    } catch (SQLException sqle) {
      // normal error
    }

    assureReaderEqual("de🙏", clob2.getCharacterStream(3, 4));
  }

  private void assureReaderEqual(String expectedStr, Reader reader) {
    try {
      char[] expected = expectedStr.toCharArray();
      char[] readArr = new char[expected.length];
      assertEquals(expected.length, reader.read(readArr));
      assertArrayEquals(expected, readArr);
    } catch (IOException ioe) {
      ioe.printStackTrace();
      fail();
    }
  }

  @Test
  public void setCharacterStream() throws SQLException, IOException {
    final byte[] bytes = "abcde🙏fgh".getBytes(StandardCharsets.UTF_8);
    MariaDbClob clob = new MariaDbClob(bytes);
    assureReaderEqual("abcde🙏", clob.getCharacterStream(1, 7));

    Writer writer = clob.setCharacterStream(2);
    writer.write("tuvxyz", 2, 3);
    writer.flush();
    assertEquals("avxye🙏", clob.getSubString(1, 7));

    clob = new MariaDbClob(bytes);

    writer = clob.setCharacterStream(2);
    writer.write("1234567890lmnopqrstu", 1, 19);
    writer.flush();
    assertEquals("a234567890lmnopqrstu", clob.getSubString(1, 100));
  }

  @Test
  public void position() {
    MariaDbClob clob = new MariaDbClob(bytes);
    assertEquals(5, clob.position("de", 2));
    assertEquals(5, clob.position((Clob) new MariaDbClob("de".getBytes()), 2));

    clob = new MariaDbClob(bytes, 2, 10);
    assertEquals(5, clob.position("🙏", 2));
  }

  @Test
  public void setString() throws SQLException {
    final byte[] bytes = "abcd£e🙏fgh".getBytes(StandardCharsets.UTF_8);
    final MariaDbClob clob = new MariaDbClob(bytes);
    assureReaderEqual("abcd£e🙏", clob.getCharacterStream(1, 8));
    clob.setString(2, "zuv");
    assertEquals("azuv£e🙏", clob.getSubString(1, 8));
    clob.setString(9, "zzz");
    assertEquals("azuv£e🙏fzzz", clob.getSubString(1, 12));

    MariaDbClob clob2 = new MariaDbClob("abcde🙏fgh".getBytes(StandardCharsets.UTF_8), 2, 9);
    assureReaderEqual("cde🙏fg", clob2.getCharacterStream(1, 7));
    assertEquals("cde🙏fg", clob2.getSubString(1, 7));

    clob2.setString(2, "zg");
    assertEquals("czg🙏f", clob2.getSubString(1, 6));
    clob2.setString(7, "zzz");
    assertEquals("czg🙏fgzzz", clob2.getSubString(1, 12));

    assertThrowsContains(
        SQLException.class, () -> clob2.setString(2, "abcd", 2, -2), "len must be > 0");
    clob2.setString(2, "abcd", 2, 2);
    assertEquals("ccd🙏f", clob2.getSubString(1, 6));
    clob2.setString(2, "opml", 3, 200);
    assertEquals("cld🙏f", clob2.getSubString(1, 6));

    clob2.setString(5, "අข\uD800\uDFA2");
    assertEquals("cld🙏අข\uD800\uDFA2", clob2.getSubString(1, 20));
    assertEquals(9, clob2.length());
    clob2.setString(6, "ข\uD800\uDFA2");
    assertEquals("cld🙏අข\uD800\uDFA2", clob2.getSubString(1, 20));
    clob2.setString(7, "\uD800\uDFA2");
    assertEquals("cld🙏අข\uD800\uDFA2", clob2.getSubString(1, 20));
    clob2.truncate(9);
    assertEquals("cld🙏අข\uD800\uDFA2", clob2.getSubString(1, 20));
    clob2.truncate(7);
    assertEquals("cld🙏අข", clob2.getSubString(1, 20));
    clob2.truncate(6);
    assertEquals("cld🙏අ", clob2.getSubString(1, 20));
    clob2.truncate(5);
    assertEquals("cld🙏", clob2.getSubString(1, 20));
    clob2.truncate(3);
    assertEquals("cld", clob2.getSubString(1, 20));

    assertThrowsContains(
        SQLException.class, () -> clob.setString(-1, "7"), "position must be >= 0");
    assertThrowsContains(
        SQLException.class, () -> clob.setString(1, null), "cannot add null string");
    assertThrowsContains(
        SQLException.class, () -> clob.setString(-1, null, 1, 2), "cannot add null string");
    assertThrowsContains(
        SQLException.class, () -> clob.setString(0, "dd", -1, 2), "offset must be >= 0");
    assertThrowsContains(
        SQLException.class, () -> clob.getSubString(-1, 7), "position must be >= 1");
    assertThrowsContains(SQLException.class, () -> clob.getSubString(1, -7), "length must be > 0");
    assertThrowsContains(
        SQLException.class, () -> clob.setString(-2, "rrr"), "position must be >= 0");
  }

  @Test
  public void setAsciiStream() throws SQLException, IOException {
    final byte[] bytes = "abcde🙏fgh".getBytes(StandardCharsets.UTF_8);
    MariaDbClob clob = new MariaDbClob(bytes);
    assureReaderEqual("abcde🙏", clob.getCharacterStream(1, 7));

    OutputStream stream = clob.setAsciiStream(2);
    stream.write("tuvxyz".getBytes(), 2, 3);
    stream.flush();

    assertEquals("avxye🙏", clob.getSubString(1, 7));

    clob = new MariaDbClob(bytes);

    stream = clob.setAsciiStream(2);
    stream.write("1234567890lmnopqrstu".getBytes(), 1, 19);
    stream.flush();
    assertEquals("a234567890lmnopqrstu", clob.getSubString(1, 100));

    MariaDbClob clob2 = new MariaDbClob(bytes);
    InputStream stream2 = clob2.getAsciiStream();
    byte[] b = new byte[12];
    stream2.read(b);
    assertArrayEquals(bytes, b);
  }

  @Test
  public void wrongUtf8() {
    final byte[] utf8Wrong2bytes = new byte[] {0x08, (byte) 0xFF, (byte) 0x6F, (byte) 0x6F};
    final byte[] utf8Wrong3bytes =
        new byte[] {0x07, (byte) 0x0a, (byte) 0xff, (byte) 0x6F, (byte) 0x6F};
    final byte[] utf8Wrong4bytes =
        new byte[] {0x10, (byte) 0x20, (byte) 0x0a, (byte) 0xff, (byte) 0x6F, (byte) 0x6F};
    final byte[] utf8Wrong4bytes2 = new byte[] {-16, (byte) -97, (byte) -103};

    assertThrowsContains(
        UncheckedIOException.class,
        () -> new MariaDbClob(utf8Wrong2bytes).length(),
        "invalid UTF8");
    assertThrowsContains(
        UncheckedIOException.class,
        () -> new MariaDbClob(new byte[] {(byte) 225}).length(),
        "invalid UTF8");

    assertThrowsContains(
        UncheckedIOException.class,
        () -> new MariaDbClob(utf8Wrong3bytes).length(),
        "invalid UTF8");
    assertThrowsContains(
        UncheckedIOException.class,
        () -> new MariaDbClob(utf8Wrong4bytes).length(),
        "invalid UTF8");
    assertThrowsContains(
        UncheckedIOException.class,
        () -> new MariaDbClob(new byte[] {(byte) 225}).truncate(2),
        "invalid UTF8");
    assertThrowsContains(
        UncheckedIOException.class,
        () -> new MariaDbClob(utf8Wrong2bytes).truncate(2),
        "invalid UTF8");
    assertThrowsContains(
        UncheckedIOException.class,
        () -> new MariaDbClob(utf8Wrong3bytes).truncate(3),
        "invalid UTF8");
    assertThrowsContains(
        UncheckedIOException.class,
        () -> new MariaDbClob(utf8Wrong4bytes2).truncate(4),
        "invalid UTF8");
  }

  @Test
  public void setBinaryStream() throws SQLException, IOException {
    final byte[] otherBytes = new byte[] {10, 11, 12, 13};

    MariaDbClob blob = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5});
    OutputStream out = blob.setBinaryStream(2);
    out.write(otherBytes);
    assertArrayEquals(new byte[] {0, 10, 11, 12, 13, 5}, blob.getBytes(1, 6));

    MariaDbClob blob2 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5});
    OutputStream out2 = blob2.setBinaryStream(4);
    out2.write(otherBytes);
    assertArrayEquals(new byte[] {0, 1, 2, 10, 11, 12, 13}, blob2.getBytes(1, 7));

    MariaDbClob blob3 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
    OutputStream out3 = blob3.setBinaryStream(2);
    out3.write(otherBytes);
    assertArrayEquals(new byte[] {2, 10, 11, 12, 13, 0, 0}, blob3.getBytes(1, 7));

    MariaDbClob blob4 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
    OutputStream out4 = blob4.setBinaryStream(4);
    out4.write(otherBytes);
    assertArrayEquals(new byte[] {2, 3, 4, 10, 11, 12}, blob4.getBytes(1, 6));

    try {
      MariaDbClob blob5 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
      blob5.setBinaryStream(0);
    } catch (SQLException sqle) {
      // normal exception
    }
  }

  @Test
  public void setBinaryStreamOffset() throws SQLException, IOException {
    final byte[] otherBytes = new byte[] {10, 11, 12, 13};

    MariaDbClob blob = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5});
    OutputStream out = blob.setBinaryStream(2);
    out.write(otherBytes, 2, 3);
    assertArrayEquals(new byte[] {0, 12, 13, 3, 4, 5}, blob.getBytes(1, 6));

    MariaDbClob blob2 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5});
    OutputStream out2 = blob2.setBinaryStream(4);
    out2.write(otherBytes, 3, 2);
    assertArrayEquals(new byte[] {0, 1, 2, 13, 4, 5, 0}, blob2.getBytes(1, 7));

    MariaDbClob blob3 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 4);
    OutputStream out3 = blob3.setBinaryStream(2);
    out3.write(otherBytes, 2, 3);
    assertArrayEquals(new byte[] {2, 12, 13, 5, 0, 0, 0}, blob3.getBytes(1, 7));

    MariaDbClob blob4 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
    OutputStream out4 = blob4.setBinaryStream(4);
    out4.write(otherBytes, 2, 2);
    assertArrayEquals(new byte[] {2, 3, 4, 12, 13, 0}, blob4.getBytes(1, 6));

    MariaDbClob blob5 = new MariaDbClob(new byte[] {0, 1, 2, 3, 4, 5}, 2, 3);
    OutputStream out5 = blob5.setBinaryStream(4);
    out5.write(otherBytes, 2, 20);
    assertArrayEquals(new byte[] {2, 3, 4, 12, 13, 0}, blob5.getBytes(1, 6));
  }

  @Test
  public void truncate() throws SQLException {
    MariaDbClob clob = new MariaDbClob(bytes);
    clob.truncate(20);
    assertEquals("abc£de🙏f", clob.getSubString(1, 9));
    clob.truncate(8);
    assertEquals("abc£de🙏", clob.getSubString(1, 9));
    assertEquals("abc£de🙏", clob.getSubString(1, 8));
    clob.truncate(7);
    assertEquals("abc£de�", clob.getSubString(1, 9));
    clob.truncate(6);
    assertEquals("abc£de", clob.getSubString(1, 9));
    clob.truncate(4);
    assertEquals("abc£", clob.getSubString(1, 8));
    clob.truncate(3);
    assertEquals("abc", clob.getSubString(1, 8));
    clob.truncate(0);
    assertEquals("", clob.getSubString(1, 8));

    MariaDbClob clob2 = new MariaDbClob("abc£de🙏fgh".getBytes(StandardCharsets.UTF_8), 2, 10);
    clob2.truncate(20);
    assertEquals("c£de🙏f", clob2.getSubString(1, 9));
    clob2.truncate(6);
    assertEquals("c£de🙏", clob2.getSubString(1, 9));
    clob2.truncate(5);
    assertEquals("c£de�", clob2.getSubString(1, 9));
    clob2.truncate(4);
    assertEquals("c£de", clob2.getSubString(1, 9));
    clob2.truncate(0);
    assertEquals("", clob2.getSubString(1, 7));
  }

  @Test
  public void free() {
    MariaDbClob blob = new MariaDbClob(bytes);
    blob.free();
    assertEquals(0, blob.length());
  }

  @Test
  @SuppressWarnings("checkstyle:AvoidEscapedUnicodeCharacters")
  public void clobLength() throws Exception {
    Statement stmt = sharedConn.createStatement();
    try (ResultSet rs =
        stmt.executeQuery("SELECT 'ab$c', 'ab¢c', 'abहc', 'ab\uD801\uDC37c', 'ab𐍈c' from dual")) {
      while (rs.next()) {

        Clob clob1 = rs.getClob(1);
        assertEquals(4, clob1.length());

        Clob clob2 = rs.getClob(2);
        assertEquals(4, clob2.length());

        Clob clob3 = rs.getClob(3);
        assertEquals(4, clob3.length());

        Clob clob4 = rs.getClob(4);
        assertEquals(5, clob4.length());

        Clob clob5 = rs.getClob(5);
        assertEquals(5, clob5.length());

        clob1.truncate(3);
        clob2.truncate(3);
        clob3.truncate(3);
        clob4.truncate(3);
        clob5.truncate(3);

        assertEquals(3, clob1.length());
        assertEquals(3, clob2.length());
        assertEquals(3, clob3.length());
        assertEquals(3, clob4.length());
        assertEquals(3, clob5.length());

        assertEquals("ab$", clob1.getSubString(1, 3));
        assertEquals("ab¢", clob2.getSubString(1, 3));
        assertEquals("abह", clob3.getSubString(1, 3));
        assertEquals("ab�", clob4.getSubString(1, 3));
        assertEquals("ab�", clob5.getSubString(1, 3));
      }
    }
  }

  @Test
  public void equal() {
    MariaDbClob clob = new MariaDbClob(bytes);
    assertEquals(clob, clob);
    assertEquals(new MariaDbClob(bytes), clob);
    assertFalse(clob.equals(null));
    assertFalse(clob.equals(""));
    byte[] bytes = "Abc£de🙏fgh".getBytes(StandardCharsets.UTF_8);
    assertNotEquals(new MariaDbClob(bytes), clob);
    assertNotEquals(new MariaDbClob("Abc".getBytes(StandardCharsets.UTF_8)), clob);
  }
}
