/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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 org.uberfire.io;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.FileUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.uberfire.commons.lifecycle.PriorityDisposableRegistry;
import org.uberfire.io.impl.IOServiceDotFileImpl;
import org.uberfire.java.nio.base.AbstractBasicFileAttributeView;
import org.uberfire.java.nio.base.AbstractPath;
import org.uberfire.java.nio.base.AttrHolder;
import org.uberfire.java.nio.base.NeedsPreloadedAttrs;
import org.uberfire.java.nio.channels.SeekableByteChannel;
import org.uberfire.java.nio.file.FileAlreadyExistsException;
import org.uberfire.java.nio.file.NoSuchFileException;
import org.uberfire.java.nio.file.OpenOption;
import org.uberfire.java.nio.file.Path;
import org.uberfire.java.nio.file.StandardOpenOption;
import org.uberfire.java.nio.file.attribute.BasicFileAttributeView;
import org.uberfire.java.nio.file.attribute.BasicFileAttributes;
import org.uberfire.java.nio.file.attribute.FileAttribute;
import org.uberfire.java.nio.file.attribute.FileTime;

import static org.junit.Assert.*;
import static org.uberfire.java.nio.base.dotfiles.DotFileUtils.dot;

/**
 *
 */
public abstract class CommonIOServiceDotFileTest {

    protected static final List<File> tempFiles = new ArrayList<File>();
    protected static IOService ioService = null;
    protected final Date dateValue = new Date();

    public static File createTempDirectory()
            throws IOException {
        final File temp = File.createTempFile("temp",
                                              Long.toString(System.nanoTime()));
        if (!(temp.delete())) {
            throw new IOException("Could not delete temp file: " + temp.getAbsolutePath());
        }

        if (!(temp.mkdir())) {
            throw new IOException("Could not create temp directory: " + temp.getAbsolutePath());
        }

        tempFiles.add(temp);

        return temp;
    }

    @AfterClass
    @BeforeClass
    public static void cleanup() {
        for (final File tempFile : tempFiles) {
            FileUtils.deleteQuietly(tempFile);
        }
    }

    @Test
    public void testFile() throws IOException {
        final Path path = getFilePath();
        ioService().write(path,
                          "ooooo!",
                          Collections.<OpenOption>emptySet(),
                          new FileAttribute<Object>() {
                              @Override
                              public String name() {
                                  return "custom";
                              }

                              @Override
                              public Object value() {
                                  return dateValue;
                              }
                          },
                          new FileAttribute<String>() {
                              @Override
                              public String name() {
                                  return "int.hello";
                              }

                              @Override
                              public String value() {
                                  return "world";
                              }
                          },
                          new FileAttribute<Integer>() {
                              @Override
                              public String name() {
                                  return "int";
                              }

                              @Override
                              public Integer value() {
                                  return 10;
                              }
                          }
        );

        Map<String, Object> attrs = ioService().readAttributes(path);

        assertEquals(testFileAttrSize1(),
                     attrs.size());
        assertTrue(attrs.containsKey("int.hello"));
        assertTrue(attrs.containsKey("custom"));
        assertTrue(attrs.containsKey("int"));

        assertEquals(10,
                     attrs.get("int"));
        assertEquals(dateValue,
                     attrs.get("custom"));
        assertEquals("world",
                     attrs.get("int.hello"));

        if (path instanceof AttrHolder) {
            ((AttrHolder) path).getAttrStorage().clear();
        }

        attrs = ioService().readAttributes(path);

        assertEquals(10,
                     attrs.get("int"));
        assertEquals(dateValue,
                     attrs.get("custom"));
        assertEquals("world",
                     attrs.get("int.hello"));

        final Map<String, Object> attrsValue = ioService().readAttributes(path);

        assertEquals(testFileAttrSize2(),
                     attrsValue.size());

        ioService().setAttributes(path,
                                  new FileAttribute<Object>() {
                                      @Override
                                      public String name() {
                                          return "my_new_key";
                                      }

                                      @Override
                                      public Object value() {
                                          return null;
                                      }
                                  });

        final Map<String, Object> attrsValue2 = ioService().readAttributes(path);

        assertEquals(testFileAttrSize3(),
                     attrsValue2.size());
        assertFalse(attrsValue2.containsKey("my_new_key"));

        ioService().delete(path);

        ioService().write(path,
                          "ooooo!");

        final Map<String, Object> attrsClean = ioService().readAttributes(path);

        assertEquals(testFileAttrSize4(),
                     attrsClean.size());
    }

    protected abstract int testFileAttrSize4();

    protected abstract int testFileAttrSize3();

    protected abstract int testFileAttrSize2();

    protected abstract int testFileAttrSize1();

    @Test
    public void testDirectory() throws IOException {
        final Path path = getDirectoryPath();
        ioService().createDirectory(path,
                                    new FileAttribute<Object>() {
                                        @Override
                                        public String name() {
                                            return "custom";
                                        }

                                        @Override
                                        public Object value() {
                                            return dateValue;
                                        }
                                    },
                                    new FileAttribute<String>() {
                                        @Override
                                        public String name() {
                                            return "int.hello";
                                        }

                                        @Override
                                        public String value() {
                                            return "world";
                                        }
                                    },
                                    new FileAttribute<Integer>() {
                                        @Override
                                        public String name() {
                                            return "int";
                                        }

                                        @Override
                                        public Integer value() {
                                            return 10;
                                        }
                                    }
        );

        Map<String, Object> attrs = ioService().readAttributes(path);

        assertEquals(testDirectoryAttrSize1(),
                     attrs.size());
        assertTrue(attrs.containsKey("int.hello"));
        assertTrue(attrs.containsKey("custom"));
        assertTrue(attrs.containsKey("int"));

        assertEquals(10,
                     attrs.get("int"));
        assertEquals(dateValue,
                     attrs.get("custom"));
        assertEquals("world",
                     attrs.get("int.hello"));

        if (path instanceof AttrHolder) {
            ((AttrHolder) path).getAttrStorage().clear();
        }

        attrs = ioService().readAttributes(path);

        assertEquals(10,
                     attrs.get("int"));
        assertEquals(dateValue,
                     attrs.get("custom"));
        assertEquals("world",
                     attrs.get("int.hello"));

        ioService().delete(path);

        ioService().createDirectory(path);

        final Map<String, Object> attrsClean = ioService().readAttributes(path);

        assertEquals(testDirectoryAttrSize4(),
                     attrsClean.size());
    }

    protected abstract int testDirectoryAttrSize4();

    protected abstract int testDirectoryAttrSize3();

    protected abstract int testDirectoryAttrSize2();

    protected abstract int testDirectoryAttrSize1();

    @Test
    public void testDelete() throws IOException {
        final Path dir = getDirectoryPath();

        ioService().createDirectory(dir,
                                    new FileAttribute<Object>() {
                                        @Override
                                        public String name() {
                                            return "custom";
                                        }

                                        @Override
                                        public Object value() {
                                            return dateValue;
                                        }
                                    },
                                    new FileAttribute<String>() {
                                        @Override
                                        public String name() {
                                            return "int.hello";
                                        }

                                        @Override
                                        public String value() {
                                            return "world";
                                        }
                                    },
                                    new FileAttribute<Integer>() {
                                        @Override
                                        public String name() {
                                            return "int";
                                        }

                                        @Override
                                        public Integer value() {
                                            return 10;
                                        }
                                    }
        );
        assertTrue(ioService().exists(dir));
        assertTrue(ioService().exists(dot(dir)));

        ioService().delete(dir);

        assertFalse(ioService().exists(dir));
        assertFalse(ioService().exists(dot(dir)));

        final Path file = getFilePath();

        ioService().write(file,
                          "ooooo!",
                          Collections.<OpenOption>emptySet(),
                          new FileAttribute<Object>() {
                              @Override
                              public String name() {
                                  return "custom";
                              }

                              @Override
                              public Object value() {
                                  return dateValue;
                              }
                          });

        assertTrue(ioService().exists(file));
        assertTrue(ioService().exists(dot(file)));

        assertFalse(((AttrHolder) file).getAttrStorage().getContent().isEmpty());

        ioService().delete(file);

        assertTrue(((AttrHolder) file).getAttrStorage().getContent().isEmpty());

        assertFalse(ioService().exists(file));
        assertFalse(ioService().exists(dot(file)));
    }

    @Test
    public void testCopyFile() {
        final Path sfile = getFilePath();
        final Path tfile = getTargetPath();

        ioService().deleteIfExists(sfile);
        ioService().deleteIfExists(tfile);

        ioService().write(sfile,
                          "wow",
                          Collections.<OpenOption>emptySet(),
                          new FileAttribute<Object>() {
                              @Override
                              public String name() {
                                  return "custom";
                              }

                              @Override
                              public Object value() {
                                  return dateValue;
                              }
                          });

        assertTrue(ioService().exists(sfile));
        assertTrue(ioService().exists(dot(sfile)));
        assertFalse(ioService().exists(tfile));
        assertFalse(ioService().exists(dot(tfile)));

        ioService().copy(sfile,
                         tfile);

        assertTrue(ioService().exists(sfile));
        assertTrue(ioService().exists(dot(sfile)));
        assertTrue(ioService().exists(tfile));
        assertTrue(ioService().exists(dot(tfile)));
    }

    @Test
    public void createDirectories() {
        final Path dir = getComposedDirectoryPath();

        assertFalse(ioService().exists(dir));

        ioService().createDirectories(dir,
                                      new FileAttribute<Object>() {
                                          @Override
                                          public String name() {
                                              return "custom";
                                          }

                                          @Override
                                          public Object value() {
                                              return dateValue;
                                          }
                                      });

        assertTrue(ioService().exists(dir));

        assertTrue(ioService().exists(dir.getParent()));
        assertNotNull(ioService().exists(dir.getParent().getFileName()));

        Map<String, Object> attrs = ioService().readAttributes(dir);

        assertEquals(createDirectoriesAttrSize(),
                     attrs.size());

        ioService().delete(dir);

        ioService().exists(dir.getParent());
    }

    protected abstract int createDirectoriesAttrSize();

    @Test
    public void testDeleteIfExistis() throws IOException {
        final Path dir = getDirectoryPath();

        ioService().deleteIfExists(dir);

        ioService().createDirectory(dir,
                                    new FileAttribute<Object>() {
                                        @Override
                                        public String name() {
                                            return "custom";
                                        }

                                        @Override
                                        public Object value() {
                                            return dateValue;
                                        }
                                    },
                                    new FileAttribute<String>() {
                                        @Override
                                        public String name() {
                                            return "int.hello";
                                        }

                                        @Override
                                        public String value() {
                                            return "world";
                                        }
                                    },
                                    new FileAttribute<Integer>() {
                                        @Override
                                        public String name() {
                                            return "int";
                                        }

                                        @Override
                                        public Integer value() {
                                            return 10;
                                        }
                                    }
        );

        assertTrue(ioService().deleteIfExists(dir));
        assertFalse(ioService().deleteIfExists(dir));

        final Path file = getFilePath();

        ioService().deleteIfExists(file);

        ioService().write(file,
                          "ooooo!",
                          Collections.<OpenOption>emptySet(),
                          new FileAttribute<Object>() {
                              @Override
                              public String name() {
                                  return "custom";
                              }

                              @Override
                              public Object value() {
                                  return dateValue;
                              }
                          });

        assertFalse(((AttrHolder) file).getAttrStorage().getContent().isEmpty());

        assertTrue(ioService().deleteIfExists(file));

        assertTrue(((AttrHolder) file).getAttrStorage().getContent().isEmpty());

        assertFalse(ioService().deleteIfExists(file));
    }

    @Test
    public void testReadNewByteChannel() throws IOException {
        final Path file = getFilePath();
        ioService().deleteIfExists(file);
        assertFalse(ioService().exists(file));
        String content = "sample content";
        ioService.write(file,
                        content);
        assertTrue(ioService().exists(file));

        final SeekableByteChannel sbc = ioService().newByteChannel(file,
                                                                   StandardOpenOption.READ);
        String readContent = readSbc(sbc);
        sbc.close();

        assertEquals(content,
                     readContent);

        ioService().delete(file);
    }

    @Test
    public void testNewByteChannel() throws IOException {
        final Path file = getFilePath();

        ioService().deleteIfExists(file);

        assertFalse(ioService().exists(file));

        final SeekableByteChannel sbc = ioService().newByteChannel(file,
                                                                   Collections.<OpenOption>emptySet(),
                                                                   new FileAttribute<Object>() {
                                                                       @Override
                                                                       public String name() {
                                                                           return "custom";
                                                                       }

                                                                       @Override
                                                                       public Object value() {
                                                                           return dateValue;
                                                                       }
                                                                   });

        sbc.write(ByteBuffer.wrap("helloWorld!".getBytes()));
        sbc.close();

        assertTrue(ioService().exists(file));

        Map<String, Object> attrs = ioService().readAttributes(file);

        assertEquals(testNewByteChannelAttrSize(),
                     attrs.size());

        try {
            ioService().newByteChannel(file,
                                       Collections.<OpenOption>emptySet());
            fail("FileAlreadyExistsException expected");
        } catch (FileAlreadyExistsException ex) {
        }

        ioService().delete(file);

        ioService().newByteChannel(file,
                                   Collections.<OpenOption>emptySet()).close();

        assertTrue(ioService().deleteIfExists(file));
    }

    protected abstract int testNewByteChannelAttrSize();

    @Test
    public void testGetAttribute() {
        final Path file = getFilePath();

        ioService().deleteIfExists(file);

        ioService().write(file,
                          "ooooo!",
                          Collections.<OpenOption>emptySet(),
                          new FileAttribute<Object>() {
                              @Override
                              public String name() {
                                  return "dcore.author";
                              }

                              @Override
                              public Object value() {
                                  return "AuthorName";
                              }
                          });

        assertNotNull(ioService().getAttribute(file,
                                               "dcore:dcore.author"));
        assertNull(ioService().getAttribute(file,
                                            "dcore:dcore.not_here"));
        assertNotNull(ioService().getAttribute(file,
                                               "dcore.author"));
        assertNull(ioService().getAttribute(file,
                                            "something"));

        ((AttrHolder) file).getAttrStorage().clear();

        assertNotNull(ioService().getAttribute(file,
                                               "dcore:dcore.author"));
        assertNull(ioService().getAttribute(file,
                                            "dcore:dcore.not_here"));
        assertNotNull(ioService().getAttribute(file,
                                               "dcore.author"));
        assertNull(ioService().getAttribute(file,
                                            "something"));
    }

    @Test
    public void testGetAttributeView() {
        final Path file = getFilePath();

        ioService().deleteIfExists(file);

        ioService().write(file,
                          "ooooo!",
                          Collections.<OpenOption>emptySet(),
                          new FileAttribute<Object>() {
                              @Override
                              public String name() {
                                  return "dcore.author";
                              }

                              @Override
                              public Object value() {
                                  return "AuthorName";
                              }
                          });

        assertNotNull(ioService().getFileAttributeView(file,
                                                       BasicFileAttributeView.class));
        assertNull(ioService().getFileAttributeView(file,
                                                    MyAttrsView.class));
        assertNotNull(ioService().getFileAttributeView(file,
                                                       XDublinCoreView.class));

        final DublinCoreAttributes attr = ioService().getFileAttributeView(file,
                                                                           XDublinCoreView.class).readAttributes();
        assertEquals("AuthorName",
                     attr.getAuthor());

        ((AttrHolder) file).getAttrStorage().clear();

        assertNotNull(ioService().getFileAttributeView(file,
                                                       BasicFileAttributeView.class));
        assertNull(ioService().getFileAttributeView(file,
                                                    MyAttrsView.class));
        assertNotNull(ioService().getFileAttributeView(file,
                                                       XDublinCoreView.class));
    }

    @Test(expected = NoSuchFileException.class)
    public void testReadAllStringFromUnexistentFile() {
        String content = ioService().readAllString(getFilePath());
    }

    @Test
    public void testReadAllStringFromEmptyFile() {
        final Path filePath = getFilePath();
        ioService().createFile(filePath);

        String content = ioService().readAllString(filePath);

        assertEquals("",
                     content);
    }

    @Test
    public void testReadAllStringFromExistentFile() {
        final Path filePath = getFilePath();
        ioService().createFile(filePath);
        ioService().write(filePath,
                          "text");

        String content = ioService().readAllString(filePath);

        assertEquals("text",
                     content);
    }

    public abstract Path getFilePath();

    public abstract Path getTargetPath();

    public abstract Path getDirectoryPath();

    public abstract Path getComposedDirectoryPath();

    private String readSbc(SeekableByteChannel sbc) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(100);
        StringBuilder content = new StringBuilder();
        byteBuffer.clear();
        try {
            while ((sbc.read(byteBuffer)) > 0) {
                byteBuffer.flip();
                content.append(new String(byteBuffer.array(),
                                          0,
                                          byteBuffer.remaining()));
                byteBuffer.compact();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return content.toString();
    }

    public IOService ioService() {
        if (ioService == null) {
            ioService = new IOServiceDotFileImpl();
            assertTrue(PriorityDisposableRegistry.getDisposables().contains(ioService));
        }
        return ioService;
    }

    private static interface MyAttrsView extends BasicFileAttributeView {

    }

    public static class XDublinCoreView extends AbstractBasicFileAttributeView<AbstractPath>
            implements NeedsPreloadedAttrs {

        private BasicFileAttributes attrs = null;

        public XDublinCoreView(final AbstractPath path) {
            super(path);
        }

        @Override
        public <T extends BasicFileAttributes> T readAttributes() throws org.uberfire.java.nio.IOException {
            if (attrs == null) {
                final BasicFileAttributes basicAtts = ((BasicFileAttributeView) path.getAttrView(BasicFileAttributeView.class)).readAttributes();
                attrs = new DublinCoreAttributes(basicAtts,
                                                 (String) path.getAttrStorage().getContent().get("dcore.author"));
            }
            return (T) attrs;
        }

        @Override
        public Class<? extends BasicFileAttributeView>[] viewTypes() {
            return new Class[]{XDublinCoreView.class};
        }
    }

    public static class DublinCoreAttributes implements BasicFileAttributes {

        private final BasicFileAttributes attributes;
        private final String author;

        private DublinCoreAttributes(final BasicFileAttributes attributes,
                                     final String author) {
            this.attributes = attributes;
            this.author = author;
        }

        public String getAuthor() {
            return author;
        }

        @Override
        public FileTime lastModifiedTime() {
            return attributes.lastModifiedTime();
        }

        @Override
        public FileTime lastAccessTime() {
            return attributes.lastAccessTime();
        }

        @Override
        public FileTime creationTime() {
            return attributes.creationTime();
        }

        @Override
        public boolean isRegularFile() {
            return attributes.isRegularFile();
        }

        @Override
        public boolean isDirectory() {
            return attributes.isDirectory();
        }

        @Override
        public boolean isSymbolicLink() {
            return attributes.isSymbolicLink();
        }

        @Override
        public boolean isOther() {
            return attributes.isOther();
        }

        @Override
        public long size() {
            return attributes.size();
        }

        @Override
        public Object fileKey() {
            return attributes.fileKey();
        }
    }
}
