#include "fesa/io/abaqus/input_reader.hpp"

#include <gtest/gtest.h>

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>

namespace {

class TemporaryInputFile {
public:
    TemporaryInputFile(const std::string& stem, const std::string& content)
        : path_{std::filesystem::temp_directory_path() /
                ("fesa-" + stem + ".inp")} {
        std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
        stream.write(content.data(), static_cast<std::streamsize>(content.size()));
        if (!stream) {
            throw std::runtime_error{"Unable to create INP reader test fixture."};
        }
    }

    ~TemporaryInputFile() {
        std::error_code error;
        std::filesystem::remove(path_, error);
    }

    const std::filesystem::path& path() const noexcept {
        return path_;
    }

private:
    std::filesystem::path path_;
};

std::string readExactBytes(const std::filesystem::path& path) {
    std::ifstream stream{path, std::ios::binary};
    if (!stream) {
        throw std::runtime_error{"Unable to read legacy INP fixture."};
    }
    return std::string{
        std::istreambuf_iterator<char>{stream},
        std::istreambuf_iterator<char>{}};
}

std::filesystem::path repositoryRoot() {
    auto path = std::filesystem::path{__FILE__}.parent_path();
    for (int parent = 0; parent < 4; ++parent) {
        path = path.parent_path();
    }
    return path;
}

} // namespace

TEST(InpSyntax, RejectsMalformedOrOrphanData) {
    const auto missingPath =
        std::filesystem::temp_directory_path() / "fesa-missing-input.inp";
    std::error_code removeError;
    std::filesystem::remove(missingPath, removeError);

    const auto unreadable = fesa::AbaqusInputReader{}.read(missingPath);
    ASSERT_FALSE(unreadable.hasValue());
    EXPECT_EQ(
        unreadable.status().failureCategory(),
        fesa::FailureCategory::input);
    ASSERT_EQ(unreadable.status().diagnostics().size(), 1U);
    EXPECT_EQ(unreadable.status().diagnostics()[0].code,
              "input-file-unreadable");

    const TemporaryInputFile malformed{"malformed-keyword", "*, name=value\n"};
    const auto malformedResult =
        fesa::AbaqusInputReader{}.read(malformed.path());
    ASSERT_FALSE(malformedResult.hasValue());
    EXPECT_EQ(
        malformedResult.status().failureCategory(),
        fesa::FailureCategory::input);
    ASSERT_EQ(malformedResult.status().diagnostics().size(), 1U);
    EXPECT_EQ(malformedResult.status().diagnostics()[0].code,
              "malformed-keyword");
    EXPECT_EQ(malformedResult.status().diagnostics()[0].location.line, 1U);

    const TemporaryInputFile orphan{
        "orphan-data", "** comment\n\norphan, data\n"};
    const auto orphanResult = fesa::AbaqusInputReader{}.read(orphan.path());
    ASSERT_FALSE(orphanResult.hasValue());
    EXPECT_EQ(
        orphanResult.status().failureCategory(),
        fesa::FailureCategory::input);
    ASSERT_EQ(orphanResult.status().diagnostics().size(), 1U);
    EXPECT_EQ(orphanResult.status().diagnostics()[0].code,
              "orphan-data-line");
    EXPECT_EQ(orphanResult.status().diagnostics()[0].location.line, 3U);
}

TEST(InpSyntax, ReadsLegacyCantileverWithoutMutation) {
    const auto inputPath =
        repositoryRoot() / "reference" / "cantilever beam" /
        "cantilever beam.inp";
    const auto bytesBefore = readExactBytes(inputPath);
    const auto timestampBefore = std::filesystem::last_write_time(inputPath);

    const auto result = fesa::AbaqusInputReader{}.read(inputPath);

    ASSERT_TRUE(result.hasValue());
    EXPECT_EQ(result.value().sourceContentIdentity,
              "fnv1a64:04543464cc970405");
    EXPECT_EQ(result.value().sourcePath,
              std::filesystem::absolute(inputPath).lexically_normal());
    ASSERT_EQ(result.value().blocks.size(), 30U);
    EXPECT_EQ(result.value().blocks.front().canonicalName, "HEADING");
    EXPECT_EQ(result.value().blocks.front().location.line, 1U);
    EXPECT_EQ(result.value().blocks.back().canonicalName, "END STEP");

    const auto element = std::find_if(
        result.value().blocks.begin(),
        result.value().blocks.end(),
        [](const fesa::KeywordBlock& block) {
            return block.canonicalName == "ELEMENT";
        });
    ASSERT_NE(element, result.value().blocks.end());
    ASSERT_EQ(element->parameters.size(), 1U);
    EXPECT_EQ(element->parameters[0].name, "TYPE");
    ASSERT_TRUE(element->parameters[0].value.has_value());
    EXPECT_EQ(*element->parameters[0].value, "B33");
    EXPECT_EQ(element->data.size(), 10U);

    EXPECT_EQ(readExactBytes(inputPath), bytesBefore);
    EXPECT_EQ(std::filesystem::last_write_time(inputPath), timestampBefore);
}
